摘自计算机世界

如何应用TCP/IP的套接字
开发网络通信应用程序

中国银行西安市分行电脑处 胡 旋

  进入九十年代后,随着计算机和网络技术的发展,很多数据处理系统都采用开放系统结构的客户机/服务器网络模式,即客户机提出任务请求,通过网络发送给服务器,由服务器做相应处理,执行被请求的任务,然后将结果返回给客户机。例如:银行ATM的前置机和数据处理的主机之间即构成客户机/服务器网络模式;电话银行的前置机和银行数据处理机之间也构成这种网络模式结构等。这样,如何在前置机和数据主机之间进行信息交换,即进程网络通信,就成为实现这种网络模式的基础。而TCP/IP的套接字技术是解这一问题的有力工具。它从提出时就一直发挥着愈来愈重要的作用,并已成为UNIX操作系统下TCP/IP网络编程标准;甚至WINDOW、JAVA都配有它的通用接口。有了这个强有力的工具,我们可以实现异种机、异种操作系统应用程序间的相互连接和通信。

  套接字(sockets)是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点。它构成了在单个主机内及整个网际间的编程界面。一般来说,跨机应用进程之间要在网络环境下进行通信,必须要在网络的每一端都要建立一个套接字,两个套接字之间是可以建立连接的,也是可以无连接的,并通过对套接字的“读”、“写”操作实现网络通信功能。类似于UNIX系统中的I/O概念,像文件那样有打开、读、写、关闭的方式。根据传输数据类型的不同,套按字可分为面向连接的数据套接字(stream sockets)和无连接的数据报套接字(datagram sockets)两种类型。

1、字节流套接字
  字节流不按记录定界,在TCP/IP协议簇中对应TCP协议,即传输控制协议(Transmition Control Protocol)。它是一个提供给用户进程可靠的全双工的面向连接的协议,大多数INTERNET应用程序如ftp、telnet使用TCP协议。通信端点使用TCP对应的INTERNET地址互相连接,可保证按正确的顺序以及单一和可靠的地址传输数据。由于它是字节流,所以包长包没有限制,信包传输也不重复,因而是一种常用的套接字类型。

2、数据报套接字
  数据报对应记录型数据流,在TCP/IP协议簇中对应UDP协议,即用户数据报协议(User Datagram Protocol)。利用数据报服务可实现一些简单的网络服务,如网点检测程序PING。由于不建立连接,数据报协议比连接协议快。但不能保证所有数据都准确有序地到达目的地。不保证顺序性、可靠性和无重复性。它是无连接的服务,以独立的信包进行传输,通信端点使用UDP对应的INTERNET地址。双方不需互连,按固定的最大长度进行传输,因而适用于单个报文传输,或较小文件的传输。

套接字的编程要点及和过程

  不论何种套接字编程,均采用客户机/服务器方式,其运作过程基本类似,限于篇幅,这里仅介绍字节流套接字。字节流套按字的服务进程和客户进程,在通信前必须创建各自的套接字以建立连接,然后对相应的套接字进行“读”、“写”操作,实现信息的交换。

  1.服务器进程创建套接字。服务进程总是先于客户进程启动,服务进程首先调用socket()函数创建自已端的一个字节流套接字,并提供三个参数:网络地址类型,一般取AF_INEF(Adress family InterNET);套接字类型,这里取SOCK_STREAM;网络协议,缺省为TCP/IP协议,对应参数为0。
  2.给套接字地址变量赋初值。在生成套接字后,要用服务器的地址先对sockaddr_in结构变量赋初值。sockaddr_in在/usr/include/netinet/in.h中有定义,它只适用于INTERNET地址类型,含有INTERNET套接字地址类型、IP端口号、IP地
址等信息。地址类型可取定为AF_INET,IP地址对服务器可取任意合法地址INADDR_ANY。IP端口号可由用户设定,但要注意主机字节顺序向网络字节顺序的转换。
  3.给套接字命名。由socket()函数创建的套接字是没有名字的。所谓命名,就是用bind()函数将服务器地址捆绑到创建的套接字上。
  4.服务器进程准备接受来自客户机的连接请求。首先调用listen()函数,让服务器进程进入监听状态;然后调用accept()函数,准备接受客户机的连接信号。无连接请求时,服务进程被阻塞。
  5.客户进程调用socket()函数创建已端的套接字。
  6.给客户端的sockaddr_in结构体变量赋值。地址类型仍可取AF_INET,端口号和服务器方的端口号相同,欲连服务器的地址通过调用inet_addr()转换得到。也可通过gethostbyname()函数将名字转换为指向hostent结构变量的指针,再将hostent结构变量的地址成员用bcopy()复制到sockaddr_in结构变量上。
  7.客户方调用connect()函数向服务进程发出连接请求。
  8.当连接请求到来后,被阻塞服务进程的accpet()函数生成一个新的字节流套接字,并返回客户机的sockaddr_in结构变量,从而在服务器应用程序中用新的被赋予客户机地址的套接字同客户进程进行连接,然后向客户方返回接受信号。
  9.一旦客户机的套接字收到来自服务器的接受信号,则表示客户机与服务器双方已实现连接。任一方均可向对方发送,也可接收对方发来的数据。这既可通过send()、recv()函数来实现。也可通过read()、write()函数来交换数据。
  10.服务进程和客户进程可通过调用shutdown()和colse()关闭套接字上的所有发送和接收操作,撤销套接字并中断连接。

  上述所有系统函数均包括在libsocket.a系统库中,IBMPC(UNIX)操作系统编译时需加-llibsocket链接。IBMRS/6000(AIX)操作系统统编译时无需此参数。

  整个运作过程可用图表示如下:

net4-1.gif (3802 字节)

编程示例

  本文给出一个运用字节流套接字,在TCP/IP网络上实现客户机/服务器方式进程通信的实例,该程序在IBMRS/6000小型机和IBMPC586微机上调试通过。它主要模拟根据帐号查询余额的过程,客户机是一台电话银行的前置机(IBM PC),服务器是银行数据处理主机(IBMRS/6000),包括三方面:

  1.客户机从标准输入读入帐号,并将该帐号发送给服务器。
  2.服务器接收该帐号后,进行判断是否有此帐号的数据,并将结果返回给客户机。
  3.客户机接收返回的信息,并将结果输出在标准输出上。

  服务进程需先于客户进程启动。客户进程启动时要携带服务器的IP地址,如:clientpros 23.169.1.1

  在UNIX操作系统下可用Doscp命名拷入硬盘;编译时用:

cc hxserver.c -lsocket -0 server
cc hxclient.c -lsocket -0 client

在AIX 操作系统下可用Dosread命名拷入硬盘,编译时用:
cc hxserver.c -0 server
cc hxclient.c -0 client




/*服务器端程序:hxserver.c*/

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include "tcpipop.c"



main()

{

int newsockfd, sockfd;

int clilen;

struct sockaddr_in cli_addr, serv_addr;

char info[100];

int infolen;

int rc;





if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/*创建TCP协议的字节流*/

{

fprintf(stderr, "Socket failed !!\n");

return(-1);

}

bzero((char *)&serv_addr, sizeof(serv_addr));/*服务器地址清0*/

serv_addr.sin_family = AF_INET;/*网络类型使用ARPA internet地址*/

serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

/*IP地址取公认的任意合法地址*/

serv_addr.sin_port = htons(1234);

/*设定IP端口号, 并将主机字节顺序转换为网络字节顺序*/



if ( bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0 )

/*将服务器地址信息捆绑到创建的套接字上*/

{

fprintf(stderr, "Bind failed !!\n");

return(-1);

}



if ( listen(sockfd, 5) < 0 )/*建立长度为5的监听队列从套接字上收听连接请求*/

{

fprintf(stderr, "Listen failed !!\n");

return(-1);

}

fprintf(stderr, "blocked here.....\n");



clilen = sizeof(cli_addr);

/*阻塞至客户方有连接请求到来*/

newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);

/*有连接请求时, 返回被赋予客户机地址的新套接字, 连接已建立*/

fprintf(stderr, "connected now!!!\n");

if ( newsockfd < 0 )

{

fprintf(stderr, "Accept failed!!\n");

return(-1);

}

shutdown(sockfd, 2);

close(sockfd);

memset(info,0,sizeof(info));

infolen=6;

if ((rc=TCPRECEIVE(newsockfd, info, infolen)) == -1)/*接受客户机发来的信息*/

fprintf(stderr, "TCPRecv failed !!\n");



if (!memcmp(info,"123456",6))

{/*返回帐号为123456的帐户信息*/

printf("received account no is %6.6s\n",info);

sprintf(info,"%6.6s|%8.8s|%10.2lf",info,"zhang li",123.456);

}

else

{/*否则, 返回000000表示无此帐户*/

printf("no such account no %6.6s\n",info);

sprintf(info,"%6.6s|%8.8s|%lf","000000","",0.0);

}



infolen=sizeof(info);

if ((rc=TCPSEND(newsockfd, info, infolen) )== -1) /*向客户机发送该帐户信息*/

fprintf(stderr, "TCPSend failed !!\n");



shutdown(newsockfd, 2);

close(newsockfd);



}





/*客户端程序:hxclient.c*/

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include "tcpipop.c"



typedef struct{

char accno[7];

char name[9];

double amt;

} data_type;

/* 帐户数据结构 */



main(argc,argv)

int argc;

char ** argv;

{

int i=0;

int sockfd;

struct sockaddr_in serv_addr;/*存服务器方的地址*/

char info[100];

int infolen;

data_type data;

int rc;



if (argc!=2)

{

fprintf(stderr,"client ipaddress\n");

exit(-1);

}

/*参数是服务器的IP地址*/



for ( ; i<10; i++ )/*最多连接10次*/

{

bzero((char *)&serv_addr, sizeof(serv_addr));

/*服务器地址变量清0*/



serv_addr.sin_family = AF_INET;

/*网络地址类型使用ARPA internet 地址*/

serv_addr.sin_addr.s_addr = inet_addr(argv[1]);

/*服务器IP地址转换为长整型IP地址, 并赋予地址结构变量*/

serv_addr.sin_port=htons(1234);

/*与服务器相同的IP端口号, 经主机字节顺序到网络字节顺序的转换*/

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)

/*创建TCP协议的字节流套接字*/

{

fprintf(stderr, "Client : Can not open stream socket\n");

shutdown(sockfd, 2);

close(sockfd);

return(-1);

}

if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) >= 0)

/*与服务进程建立连接*/

break;

shutdown(sockfd, 2);

close(sockfd);

}



if ( i >= 10 )

exit(-1);

/*此时连接已建立, 可通过对套接字的读写实现通信*/

memset(&data,0,sizeof(data));

printf("enter account no:");

scanf("%s",data.accno); /*输入要检索的帐号*/



infolen=sizeof(data.accno);

bcopy(&data.accno,info,infolen);

if ( (rc=TCPSEND(sockfd, info, infolen)) == -1 )

fprintf(stderr, "TCPSend failed !!\n");

/*客户进程向服务进程发送帐号信息*/

infolen=sizeof(info);

if ( (rc=TCPRECEIVE(sockfd, info, infolen)) == -1 )

fprintf(stderr, "TCPRecv failed !!\n");

/*客户进程接收服务进程返回的帐户信息*/



sscanf(info,"%[^|]|%[^|]|%lf",data.accno,data.name,&data.amt); /*格式转换*/



if (!memcmp(data.accno,"000000",6))/*判断是否查到*/

printf("There is no such account in server database.\n");

else {/*显示帐户信息*/

printf("accno is %s\n",data.accno);

printf("name is %s\n",data.name);

printf("amount is %-10.2lf\n",data.amt);

}



shutdown(sockfd, 2);

close(sockfd);

}



/*公共程序:tcpipop.c*/

/*

* Using TCP/IP Protocol.

*/



#include <stdio.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <netinet/tcp.h>



extern int TCPSEND(int fd, char *buf, int len)

{

int i=0;

int send_rc;



for ( ; i<10; i++ ) {

send_rc = write( fd, buf, len );

if ( send_rc == len )

break;

sleep(1);

}



if ( i >= 10 )

return(-1);

else

return(send_rc);

}





extern int TCPRECEIVE(int fd, char *buf, int len)

{

int i=0;

int receive_rc;



for ( ; i<10; i++ ) {



receive_rc = read(fd, buf, len);



if ( receive_rc == len )

break;

sleep(1);

}



if ( i >= 10 )

return(-1);

else

return(receive_rc);

}

back.gif (1185 字节)