Unix网络编程读书笔记(三)

来源:互联网 发布:淘宝网淋浴塑料帘布 编辑:程序博客网 时间:2024/06/06 04:18

这一章正式开始网络编程的内容,先将书中的示例编写如下:

首先是服务器端:

#include <sys/socket.h>#include <netinet/in.h>#include <unistd.h>#include <string.h>#include <stdio.h>#include <stdlib.h>#include <errno.h>#define SERV_PORT 6666#define LISTENQ 14#define MAXLINE 100void str_echo(int sockfd);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,0);bzero(&servaddr,sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));listen(listenfd,LISTENQ);for(;;){clilen = sizeof(cliaddr);connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);if((childpid=fork())==0){close(listenfd);str_echo(connfd);exit(0);}close(connfd);}return 0;}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("read error\n");exit(1);}}

然后是客户端程序:

#include <sys/socket.h>#include <netinet/in.h>#include <unistd.h>#include <string.h>#include <stdio.h>#include <stdlib.h>#include <errno.h>#define SERV_PORT 6666#define MAXLINE 100void str_cli(FILE* fp,int sockfd);ssize_t Readline(int fd,void* vptr,size_t maxlen);int main(int argc,char* argv[]){int sockfd;struct sockaddr_in servaddr;sockfd = socket(AF_INET,SOCK_STREAM,0);bzero(&servaddr,sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);inet_pton(AF_INET,argv[1],&servaddr.sin_addr);connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));str_cli(stdin,sockfd);return 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(Readline(sockfd,recvline,MAXLINE)==0){printf("str_cli:server terminated prematurely\n");exit(0);}fputs(recvline,stdout);}}ssize_t Readline(int fd,void* vptr,size_t maxlen){ssize_t n,rc;char c,*ptr;ptr = vptr;for(n=1;n<maxlen;n++){again:if((rc=read(fd,&c,1))==1){*ptr++ = c;if(c=='\n') break;}else if(rc==0){*ptr = 0;return (n-1);}else{if(errno==EINTR) goto again;return (-1);}}*ptr = 0;return (n);}

程序运行结果如下:

./src_5_2 &[1] 4512

使用netstat命令检查服务器监听套接字的状态:

netstat -a激活Internet连接 (服务器和已建立连接的)Proto Recv-Q Send-Q Local Address           Foreign Address         State      tcp        0      0 *:6666                  *:*                     LISTEN     

6666就是咱们设置的端口号,666。*代表通配地址。当前状态为LISTEN。

此时再次使用netstat命令,查看套接字状态

netstat -a | grep 6666tcp        0      0 *:6666                  *:*                     LISTEN     tcp        0      0 localhost:38280         localhost:6666          ESTABLISHEDtcp        0      0 localhost:6666          localhost:38280         ESTABLISHED

第一行的套接字代表服务器的监听套接字。

第二行的套接字代表客户端的套接字。

第三行的套接字代表服务器的已连接套接字。

写至此处我感觉在上一篇读书笔记中存在一处错误,在accept函数返回后,监听套接字仍然处于监听状态,而仅有已连接套接字处于建立状态。

最后通过ps命令对进程中的状态进行查看:

ps -t pts/8 -o pid,ppid,tty,stat,argc,command,wchan  PID  PPID TT       STAT ARGC COMMAND                     WCHAN 2363  2357 pts/8    Ss      - bash                        wait 2428  2363 pts/8    S       - ./src_5_2                   inet_csk_accept 2434  2363 pts/8    S+      - ./src_5_4 127.0.0.1         wait_woken 2435  2428 pts/8    S       - ./src_5_2                   sk_wait_data

Linux进程阻塞于accept时,输出inet_csk_accept。

Linux进程阻塞于终端I/O时,输出wait_woken。

Linux进程阻塞于套接字输入或输出时,输出sk_wait_data。

通过crtl+D正常终止客户端,再次使用netstat命令查看套接字状态。

netstat -a | grep 6666tcp        0      0 *:6666                  *:*                     LISTEN     tcp        0      0 localhost:38465         localhost:6666          TIME_WAIT  

此时客户端套接字已经进入TIME_WAIT状态。在服务器方面,监听套接字仍然处于LISTEN状态,而已连接套接字套接字则完全关闭。

让我们来回顾一下已连接套接字是如何关闭的。

首先是客户端调用exit关闭自己的描述符,则由客户打开的套接字由内核关闭。

  1. 这一过程导致客户TCP发送一个FIN给服务器,客户端套接字首先进入FIN_WAIT_1状态。
  2. 此时服务器接收FIN同时发送ACK,已连接套接字状态变为CLOSE_WAIT状态。
  3. 客户端套接字接收ACK后进入FIN_WAIT_2状态。

在服务器已连接套接字接收到FIN时,read函数返回0,注意此处客户端并没有显示的发送一个0字节的数据,服务器是通过接收到FIN而使read函数返回0的。

此时服务器的子进程返回,已连接套接字也会被关闭。由子进程来关闭已连接套接字会引发TCP连接终止序列的最后两个分节。

  1. 服务器向客户发送FIN,并进入LAST_ACK状态。
  2. 客户端接收FIN并发送ACK,此时套接字状态进入TIME_WAIT。
  3. 服务器接收客户端发来的ACK,已连接套接字进入CLOSED状态。

客户端在等待2MSL后也将进入CLOSED状态。

再来看几种出现故障的情况

1)服务器进程终止

在这种情况下,服务器的子进程被杀死,此时导致已连接套接字引用计数为0,导致TCP连接终止工作开始。但这种情况与正常终止的情况是相反的:

  1. 服务器向客户端套接字发送FIN,状态进入FIN_WAIT1。
  2. 客户接收FIN并响应一个ACK,状态进入CLOSE_WAIT。
  3. 服务器接收ACK,状态进入FIN_WAIT2。

但此时客户进程仍然阻塞于fgets函数,套接字并未关闭,因此无法完成连接终止的后半部分。

此时使用netstat命令查看套接字状态。

netstat -a | grep 6666tcp        0      0 *:6666                  *:*                     LISTEN     tcp        1      0 localhost:39881         localhost:6666          CLOSE_WAIT tcp        0      0 localhost:6666          localhost:39881         FIN_WAIT2  

此时在客户端中输入数据,程序运行结果如下:

./src_5_4 127.0.0.1 //在此之前服务器子进程已经被杀死another linestr_cli:server terminated prematurely

当我们输入“another line”时,TCP仍然会将数据发往服务器,但此时先前打开套接字的进程已经终止,于是响应以一个RST。再来看客户端程序,客户端在调用wrtite将数据发往服务器后,使用read系统调用从套接字中读出数据,但由于在套接字上已经接收到FIN,因此read系统调用返回0(表示EOF)。

2)向已收到RST的套接字执行写操作

当这种情况发生时,内核向该进程发送SIGPIPE信号。这里要与上一个例子区别开来:在上一个例子中,是向收到FIN的客户套接字再次写入数据,但向一个已接收FIN的套接字写入数据是没有问题的。但向已经关闭的已连接套接字发送数据,则会导致服务器响应RST,但向已经接收到RST的套接字写入数据则是一个错误。

将客户程序稍作修改:

void str_cli(FILE* fp,int sockfd){char sendline[MAXLINE],recvline[MAXLINE];while(fgets(sendline,MAXLINE,fp)!=NULL){write(sockfd,sendline,1);sleep(1);write(sockfd,sendline+1,strlen(sendline)-1);if(Readline(sockfd,recvline,MAXLINE)==0){printf("str_cli:server terminated prematurely\n");exit(0);}fputs(recvline,stdout);}}

将写操作变为两个是让第一个write引发RST,再让第二个write引发SIGPIPE。

运行结果如下:

./src_5_14 127.0.0.1hello worldhello worldbye

并未像书中展示的那样提示错误。






0 0