TCP客户/服务器程序示例
2017-10-29 21:07
471 查看
概述
简单的TCP回射服务器:客户从标准输入读入一行文本,并写给服务器;服务器从网络输入读取文本,并回射给客户;客户从网络输入读取文本,并显示在终端。TCP回射程序
服务器端:void server_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) err_sys("server_echo: read error"); } int main(int argc, char **argv)//客户程序 { int listenfd, connfd;//监听fd和连接fd struct sockaddr_in servaddr , cliaddr; socklen_t clilen; pid_t childpid; /*×××××××××××××××××××服务器端套路开始××××××××××××××××××××*/ listenfd = Socket(AF_INET, SOCK_STREAM, 0);//套接字创建 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET;//服务器IP地址为网卡IP地址 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(4099); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//绑定本机IP地址和端口,设置结构体成员而已 Listen(listenfd, LISTENQ);//绑定之后就可以开始监听外部连接了,指定了排队最大客户连接数 /********************服务器端套路结束*********************/ for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd , (SA *)&cliaddr , &clilen);//传入长度为了达到值-结果 if((childpid = Fork()) == 0){ Close(listenfd);//文件计数关闭一次 server_echo(connfd); Close(connfd); exit(0); }//child,并行服务器就是不停fork处理 Close(connfd);//等待下一个客户机连接。 } }
客户机端:
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) err_quit("str_cli:server terminated "); fputs(recvline , stdout); } } int main(int argc, char **argv)//客户程序 { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: a.out <IPaddress>"); if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)//创建一个网际字节流套接字,TCP套接字 err_sys("socket error"); bzero(&servaddr, sizeof(servaddr));//传递地址,将N字节清0 servaddr.sin_family = AF_INET; servaddr.sin_port = htons(4099); /* daytime server */ if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)//填入服务器IP地址和端口号13 err_quit("inet_pton error for %s", argv[1]); if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)//TCP握手操作,交换数据 err_sys("connect error"); str_cli(stdin , sockfd); exit(0); }
认真分析过程很重要理解状态转换:
正常启动和终止分析结合状态转换图
1、服务器端后台运行启动,调用Socket , Bind ,Listen,然后阻塞在Accept,等待客户机连接。这时候服务器进入Listen状态。2、运行客户机子程序,调用Socket,Connect发起三次握手操作。握手成功之后服务器和客户机全部进入ESTABLISHED状态,可以进行相互收发了。并且服务器从Accept返回,创建服务器子进程进入server_echo并行处理这个连接;客户机从Connect返回进入str_cli。另一方面服务器再次调用Accept并阻塞,等待下一个连接。
3、客户机输入Ctrl+D,表示终止。这时候客户机exit,发出一个FIN信号给服务器,并进入FIN_WAIT_1状态。服务器子进程收到信号之后,发送ACK信号给客户机,服务器进入CLOSE_WAIT状态。客户机收到ACK信号后,进入FIN_WAIT_2状态。这时候关闭操作的前半部分已经完成(这个时候服务器还可以继续处理客户机之前发送上来的数据,然后返回给客户机,四次断开就给服务器留下了一个处理尾部数据的时间,当服务器数据全部处理完了,并且也返回给客户机了,这个时候服务器就同样可以发送FIN信号了。这个完全符合我们生活之中实际的例子)。
4、服务器结束最后工作之后,也发送FIN信号。并进入LASK_ACK状态。这时候客户机收到FIN并进行响应,连接完全终止。客户机进入FIN_WAIT状态,服务器进入CLOSE状态。为什么需要FIN_WAIT状态??前面已经说的很清楚。对应服务器子进程僵死。因为父进程没有waitpid操作处理子进程。
处理僵死服务器子进程
捕获信号和waitpid处理僵死进程。在服务器端直接捕获这个信号就可以了,这样僵死进程就不会发生了,很简单的。void sig_child(int signo) { pid_t pid; while((pid = waitpid(-1 , NULL , WNOHANG)) > 0)//处理子进程,-1表示等待任何子进程,NULL不存取状态,不阻塞。收到信号处理完毕,立即返回,等待捕获下一个信号。 //这样就不会出现僵死进程 printf("child %d terminated\n" , pid); }
服务器端代码就增加了一个signal注册函数:
void server_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) err_sys("server_echo: read error"); } void sig_child(int signo) { pid_t pid; while((pid = waitpid(-1 , NULL , WNOHANG)) > 0)//处理子进程,-1表示等待任何子进程,NULL不存取状态,不阻塞。收到信号处理完毕,立即返回,等待捕获信号。 //这样就不会出现僵死进程 printf("child %d terminated\n" , pid); } int main(int argc, char **argv)//服务器程序 { int listenfd, connfd;//监听fd和连接fd struct sockaddr_in servaddr , cliaddr; socklen_t clilen; pid_t childpid; /*×××××××××××××××××××服务器端套路开始××××××××××××××××××××*/ listenfd = Socket(AF_INET, SOCK_STREAM, 0);//套接字创建 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET;//服务器IP地址为网卡IP地址 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(4099); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//绑定本机IP地址和端口,设置结构体成员而已 Listen(listenfd, LISTENQ);//绑定之后就可以开始监听外部连接了,指定了排队最大客户连接数 /********************服务器端套路结束*********************/ Signal(SIGCHLD , sig_child);//注册回调函数 for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd , (SA *)&cliaddr , &clilen);//传入长度为了达到值-结果 if((childpid = Fork()) == 0){ Close(listenfd);//文件计数关闭一次 server_echo(connfd); Close(connfd); exit(0); }//child,并行服务器就是不停fork处理 Close(connfd);//等待下一个客户机连接。 } }
异常分析结合状态转换图
通过select poll修改程序
内核一旦发现进程指定的一个或多个IO条件就绪(可读可写),它可以通知进程,这就是IO复用,select和poll支持功能。通过select就可以知道服务器终止了。就返回了select
void str_cli(FILE *fp , int sockfd)//通过select机制,使得服务器终止服务器便知道,防止阻塞在fgets上 { char sendline[MAXLINE] , recvline[MAXLINE]; fd_set rset;//定义读描述符集 int maxfdp1;//记录最大描述符加1 FD_ZERO(&rset);//将描述符集清空 for( ; ; ){ FD_SET(fileno(fp) , &rset);//通过文件描述符设置描述符集对于位 FD_SET(sockfd , &rset); maxfdp1 = max(fileno(fp) , sockfd) + 1;//最大描述符+1 select(maxfdp1 , &rset , NULL , NULL , NULL);//设置测试的读描述符集,写和异常不设置,无限等待直到描述符就绪。 if(FD_ISSET(sockfd , &rset) ){//sockfd可读(rst FIN 数据就绪) if(Readline(sockfd , recvline , MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); fputs(recvline , stdout);//正常数据,则写到标准输出 } if(FD_ISSET(fileno(fp) , &rset)){//标准输入可读,读出来然后写入 if(fgets(sendline , MAXLINE , fp) == NULL) return ; Write(sockfd , sendline , strlen(sendline)); } } }
poll
相关文章推荐
- 【UNIX网络编程(三)】TCP客户/服务器程序示例
- UNPv13:#第5章#TCP客户/服务器程序示例
- UNIX网络编程---TCP客户/服务器程序示例(五)
- UNP卷1:第五章(TCP客户/服务器程序示例)
- Unix网络编程代码 第5章 TCP客户/服务器程序示例
- TCP客户/服务器程序示例
- Linux网络编程(三) TCP客户/服务器程序示例
- TCp客户/服务器程序示例
- UNPV3第五章TCP客户/服务器程序示例
- UNP函数笔记三: TCP客户/服务器程序示例
- UNIX网络编程笔记 第五章 TCP客户/服务器程序示例
- 5. TCP客户/服务器程序示例
- UNIX网络编程 第5章 TCP客户/服务器程序示例
- TCP 客户/服务器程序示例
- UNP——Chapter 5:TCP客户/服务器程序示例
- UNP-UNIX网络编程 第五章:TCP客户/服务器程序示例
- UNIX网络编程卷一 笔记 第五章 TCP客户/服务器程序示例
- UNIX网络编程卷1:套接字联网-第5章:TCP客户/服务器程序示例
- 【UNIX网络编程】TCP客户/服务器程序示例
- UNIX网络编程---TCP客户/服务器程序示例(五)