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 100

void 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 100

void 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,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 6666
tcp        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 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

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






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值