网络编程读书笔记(三)"/>
Unix网络编程读书笔记(三)
这一章正式开始网络编程的内容,先将书中的示例编写如下:
首先是服务器端:
#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 6666
tcp 0 0 *:6666 *:* LISTEN
tcp 0 0 localhost:38280 localhost:6666 ESTABLISHED
tcp 0 0 localhost:6666 localhost:38280 ESTABLISHED
第一行的套接字代表服务器的监听套接字。
第二行的套接字代表客户端的套接字。
第三行的套接字代表服务器的已连接套接字。
写至此处我感觉在上一篇读书笔记中存在一处错误,在accept函数返回后,监听套接字仍然处于监听状态,而仅有已连接套接字处于建立状态。
最后通过ps命令对进程中的状态进行查看:
ps -t pts/8 -o pid,ppid,tty,stat,argc,command,wchanPID PPID TT STAT ARGC COMMAND WCHAN2363 2357 pts/8 Ss - bash wait2428 2363 pts/8 S - ./src_5_2 inet_csk_accept2434 2363 pts/8 S+ - ./src_5_4 127.0.0.1 wait_woken2435 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 6666
tcp 0 0 *:6666 *:* LISTEN
tcp 0 0 localhost:38465 localhost:6666 TIME_WAIT
此时客户端套接字已经进入TIME_WAIT状态。在服务器方面,监听套接字仍然处于LISTEN状态,而已连接套接字套接字则完全关闭。
让我们来回顾一下已连接套接字是如何关闭的。
首先是客户端调用exit关闭自己的描述符,则由客户打开的套接字由内核关闭。
- 这一过程导致客户TCP发送一个FIN给服务器,客户端套接字首先进入FIN_WAIT_1状态。
- 此时服务器接收FIN同时发送ACK,已连接套接字状态变为CLOSE_WAIT状态。
- 客户端套接字接收ACK后进入FIN_WAIT_2状态。
在服务器已连接套接字接收到FIN时,read函数返回0,注意此处客户端并没有显示的发送一个0字节的数据,服务器是通过接收到FIN而使read函数返回0的。
此时服务器的子进程返回,已连接套接字也会被关闭。由子进程来关闭已连接套接字会引发TCP连接终止序列的最后两个分节。
- 服务器向客户发送FIN,并进入LAST_ACK状态。
- 客户端接收FIN并发送ACK,此时套接字状态进入TIME_WAIT。
- 服务器接收客户端发来的ACK,已连接套接字进入CLOSED状态。
客户端在等待2MSL后也将进入CLOSED状态。
再来看几种出现故障的情况
1)服务器进程终止
在这种情况下,服务器的子进程被杀死,此时导致已连接套接字引用计数为0,导致TCP连接终止工作开始。但这种情况与正常终止的情况是相反的:
- 服务器向客户端套接字发送FIN,状态进入FIN_WAIT1。
- 客户接收FIN并响应一个ACK,状态进入CLOSE_WAIT。
- 服务器接收ACK,状态进入FIN_WAIT2。
但此时客户进程仍然阻塞于fgets函数,套接字并未关闭,因此无法完成连接终止的后半部分。
此时使用netstat命令查看套接字状态。
netstat -a | grep 6666
tcp 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 line
str_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.1
hello world
hello world
bye
并未像书中展示的那样提示错误。
更多推荐
Unix网络编程读书笔记(三)
发布评论