上一节给出了TCP网络编程的函数,这一节使用那些基本函数编写一个完成的TCP客户/服务器程序示例。
该例子执行的步骤如下:
1、客户从标准输入读入一行文本,并写给服务器。
2、服务器从网络输入读入这行文本,并回射给客户。
3、客户从网络输入读入这行回射文本,并显示在标准输出上。
用图描述如下:
编写TCP回射服务器程序如下:
#include <stdio.h> #include <errno.h> #include <stdlib.h> #include <strings.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define SERV_PORT 9877 #define MAXLINE 2048 void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while((n = read(sockfd, buf, MAXLINE)) > 0) write(sockfd, buf, n); if(n < 0 && errno == EINTR) goto again; else if(n < 0) printf("str_echo : read error"); } int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(listenfd == -1){ printf("socket fail.\n"); return -1; } bzero(&servaddr, sizeof(struct sockaddr_in)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))){ printf("bind fail.\n"); return -1; } if(listen(listenfd, 5)){ printf("listen fail.\n"); return -1; } printf("listen stat.\n"); for(;;){ clilen = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); if(connfd == -1){ printf("accept fail.\n"); return -1; } printf("accept stat.\n"); if((childpid = fork()) == 0){ close(listenfd); printf("servers.\n"); str_echo(connfd); exit(0); } close(connfd); } return 0; }
TCP回射客户程序如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #define MAXLINE 2048 #define SERV_PORT 9877 void str_cli(FILE *, int); int main(int argc, char **argv) { int sockfd; struct sockaddr_in cliaddr; if(argc != 2){ printf("usage:tcpcli <IPaddress>"); return -1; } sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(sockfd == -1){ printf("socket fail.\n"); return -1; } bzero(&cliaddr, sizeof(cliaddr)); cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(SERV_PORT); if(!(inet_pton(AF_INET, argv[1], &cliaddr.sin_addr))){ printf("inet pton fail.\n"); return -1; } if(connect(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr))){ printf("connect fail.\n"); return -1; } str_cli(stdin, sockfd); exit(0); } void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while(fgets(sendline, MAXLINE, fp) != NULL){ write(sockfd, sendline, strlen(sendline)); if(read(sockfd, recvline, MAXLINE) == 0) printf("str_cli:server terminated prematurely"); fputs(recvline, stdout); } }
编译两个程序:
gcc tcpserv.c -o tcpserv
gcc tcpcli.c -o tcpcli
测试,正常启动服务器./tcpserv &,之后启动客户端./tcpcli 127.0.0.1,之后就可以在终端输入一行文本,接着就会显示出该文本。
分析整个建立过程:
1、首先在后台启动服务器,服务器启动后,它调用socket、bind、listen和accept,并阻塞于accept调用。在启动客户程序之前,用netstat程序检查服务器监听套接字的状态。
netstat -a | grep 9877,显示如下内容:
tcp 0 0 *:9877 *:* LISTEN
该行表明,有一个套接字处于LISTEN状态,它有通配的本地IP地址,本地端口为9877。
2、接着在同一主机上启动客户,并指定服务器主机的IP地址为127.0.0.1(环回地址)。./tcpcli 127.0.0.1
客户调用socket和connect,connect引起TCP的三路握手过程。当三路握手完成之后,客户中的connect和服务器中的accept均返回,连接于是建立。
3、客户调用str_cli函数,该函数阻塞与fgets调用。
4、当服务器中的accept返回时,服务器调用fork,再又子进程调用str_echo。该函数调用read,而read在等待客户送入一行文本期间阻塞。
5、另一方面,服务器父进程再次调用accept并阻塞,等待下一个客户连接。
至此,有3个都在睡眠的进程:客户进程、服务器父进程和服务器子进程。
分析终止过程:
连接建立后,在客户的标准输入中键入什么,都会回射到它的标准输出中,在客户正常终止时,客户和服务器的步骤如下:
1、当我们键入EOF字符时,fgets返回一个空指针,于是str_cli函数返回。
2、当str_cli返回到客户的main函数时,main通过调用exit终止。
3、进程终止处理的部分工作是关闭所有打开的描述符,因此客户打开的套接字由内核关闭。这导致客户TCP发送一个FIN给服务器,服务器TCP则以ACK响应,这就是TCP链接终止序列的前半部分。至此,服务器套接字处于CLOSE_WAIT状态,客户套接字则处于FIN_WAIT_2状态。
4、当服务器TCP接收FIN时,服务器子进程阻塞于read调用,于是read返回0。这导致str_echo函数返回服务器子进程的main函数。
5、服务器子进程通过调用exit来终止。
6、服务器子进程中打开的所有描述符随之关闭。
7、进程终止处理的另一部分是:在服务器子进程终止时,给父进程发送一个SIGCHLD信号。
思考:
1、子进程给父进程发送了一个SIGCHLD信号,但父进程没有捕获它,而是采用了默认行为,这样会导致子进程进入僵死状态。
2、上面的都是正常启动正常终止的情况,若服务器进程在客户之前终止,则客户会发生什么?若服务器主机崩溃又会怎样?等等
【UNIX网络编程(三)】TCP客户/服务器程序示例,布布扣,bubuko.com