《unix网络编程》(14)使用select、shutdown的客户服务器程序
来源:互联网 发布:数控车床螺纹编程g97 编辑:程序博客网 时间:2024/05/17 00:03
文章《unix网络编程》(11)tcp服务器的几种常见状况分析分析了我们之前的客户服务器程序(《unix网络编程》(10)wait/waitpid处理僵死进程(SIGCHLD信号))存在的问题。
客户端
如下修改程序中,客户的str_cli函数用select重写,一旦服务器进程终止,客户就能立刻得到通知。之前程序阻塞于fgets,这里阻塞于select调用(或等待标准输入可读、或等待套接字可读)。大大提高了客户端的健壮性。
shutdown函数的使用允许我们正确批量处理输入。因为在标准输入的EOF并不意味着同时完成了从套接字的读入;可能仍然有请求在去往服务器的路上,或者仍有应答在返回客户的路上。我们需要shutdown提供半连接的TCP。也就是说,我们想给服务器发一个FIN告诉它我们完成了数据发送,但是仍然保持套接字描述符打开以便读取。
//客户端的str_cli函数void str_cli(FILE *fp, int sockfd){ //stdineof是一个初始化为0的标志,只要该标志为0, //每次在主循环中总是select标准输入的可读性 int maxfdp1, stdineof; fd_set rset; char buf[MAXLINE]; int n; stdineof = 0; FD_ZERO(&rset); for ( ; ; ) { if (stdineof == 0)FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = (fileno(fp) > sockfd ? fileno(fp) : sockfd) + 1; Select(maxfdp1, &rset, NULL, NULL, NULL); //当在套接字上读到EOF时,如果我们已经在标准输入上遇到EOF,那就是正常 //终止,函数返回。但是如果还没有在标准输入遇到EOF,那么服务器进程已经 //过早终止。只有服务器进程终止,select就会立刻通知客户端进程。 if (FD_ISSET(sockfd, &rset)) { //socket可读if ((n = Read(sockfd, buf, MAXLINE)) == 0) { if (stdineof == 1) return; else err_quit("str_cli: server terminated prematurely");}Write(fileno(stdout), buf, n); } //当在标准输入遇到EOF,把标志stdineof设为1, //并把第二个参数设置为SHUT_WR来调用shutdown以发送FIN。 if (FD_ISSET(fileno(fp), &rset)) { //stdout可读if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) { //从标准输入读到了EOF(Ctrl+C) stdineof = 1; Shutdown(sockfd, SHUT_WR); FD_CLR(fileno(fp), &rset); continue;}Writen(sockfd, buf, n); //shutdown后仍可以将缓冲区中剩余的数据全部发送到套接字 } }}
服务器端
将服务器端程序改为用select来处理任意客户的单进程程序,而不是为每个客户派生一个子进程。它可以避免为每个客户端建立一个进程的开销。但没法阻止拒绝服务攻击,看下一节。
#include "myheader.h"int main(int argc, char **argv){ int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; //client数组含有每个客户的已连接套接字描述符,初始化为-1 ssize_t n; fd_set rset, allset; char buf[MAXLINE]; char cliip[MAXLINE]; //作为inet_ntop的第三个参数指针,接收点分十进制的ip socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (const struct sockaddr*)&servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); maxfd = listenfd; maxi = -1; for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; FD_ZERO(&allset); FD_SET(listenfd, &allset); for ( ; ; ) { //select等待某事件发生:或是新客户连接建立,或是数据、FIN或RST到达 rset = allset; nready = Select(maxfd + 1, &rset, NULL, NULL, NULL); //如果某个监听套接字可读,那么建立一个新连接。 //调用accept并相应更新数据结构,使用client数组中 //第一个未用记录这个已连接描述符。就绪描述符数目减1,若其值为0, //就可以避免进入下一个for循环。这样做让我们可以使用select的返回值 //来避免检查未就绪的描述符 if (FD_ISSET(listenfd, &rset)) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (const struct sockaddr*)&cliaddr, &clilen); printf("new client: %s, port %d \n", Inet_ntop(AF_INET, &cliaddr.sin_addr, cliip, sizeof(buf)), ntohs(cliaddr.sin_port)); for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) { client[i] = connfd; break;} if (i == FD_SETSIZE)err_quit("too many clients"); FD_SET(connfd, &allset); if (connfd > maxfd)maxfd = connfd; if (i > maxi)maxi = i; if (--nready <= 0)continue; } //对于每个现有的客户连接,要测试其描述符是否在select返回的描述符集中。 //如果是就从该客户读入一行文本并回射给它。如果该客户已经关闭连接,read //返回0,更新数据结构。 for (i = 0; i <= maxi; i++) { if ((sockfd = client[i]) < 0)continue; if (FD_ISSET(sockfd, &rset)) { if ((n = Read(sockfd, buf, MAXLINE)) == 0) { Close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1;}else Writen(sockfd, buf, n);if (--nready <= 0) break; } } }}
拒绝服务攻击
对于上述程序,如果某个恶意客户连接到服务器后发送一个字节后休眠。服务器会read该字节,然后阻塞于下一个read调用,以等待来自该客户的其余数据。服务器因为一个客户阻塞不能为其它客户服务,直到那个恶意客户发出换行符或终止为止。
解决方法:(1)使用非阻塞I/O;
(2)让每个客户由单独控制线程提供服务(为每个客户建立子进程或线程);
(3)对I/O操作设置一个超时。
完整源码
http://download.csdn.net/detail/u013074465/8566567
github:服务器端源码 https://github.com/liyangddd/linux_practice/blob/master/network_programming/tcpsrvselect.c
客户端源码 https://github.com/liyangddd/linux_practice/blob/master/network_programming/tcpcliselect.c
演示结果
如果客户发送RST
该服务器程序接收到 一个RST后,read返回错误,我们的Read包裹函数时终止服务器。因此,这样的服务器程序太脆弱了,因为一个用户的RST就退出。通常服务器不应该因为这个原因而终止。服务器应该登记错误,关闭出错的套接字,并继续服务其他用户。简单终止服务器是不能接受的。该服务器是单进程的,如果是每个进程或线程服务一个用户,那么线程或进程退出不会有太大影响。
- 《unix网络编程》(14)使用select、shutdown的客户服务器程序
- 《unix网络编程》(15)poll函数以及使用poll的客户服务器程序
- UNIX网络编程学习(15)--使用单进程和select的TCP服务器程序
- UNIX网络编程---TCP客户/服务器程序示例(五)
- 【UNIX网络编程(三)】TCP客户/服务器程序示例
- UNIX网络编程——使用select函数的TCP和UDP回射服务器程序
- 《unix网络编程》(13)select、shutdown函数
- 【Unix 网络编程】TCP 客户/服务器简单 Socket 程序
- 【Unix 网络编程】UDP 客户/服务器简单 Socket 程序
- Unix网络编程 深入探索TCP客户/服务器程序
- UNIX网络编程(10)--TCP 回射服务器程序和客户程序
- UNIX网络编程笔记(4)—TCP客户/服务器程序示例
- 《UNIX网络编程 卷1》 笔记: 使用select函数的单进程TCP回射服务器程序
- 【UNIX网络编程】客户/服务器程序设计范式
- UNIX网络编程——使用waitpid处理僵尸进程(TCP客户/服务器优化1)
- unix网络编程 select函数和shutdown函数
- 简单的获取服务器时间程序(UNIX网络编程)
- 学习 UNIX网络编程卷1:套接字 笔记1-实现一个简单的回射客户服务器程序
- 玩命牛的成长记录(六)——定位
- 伸展树实现
- UCOS试验一 LED灯切换闪烁
- 你凭什么说自己很努力
- 第四周 项目1-三角形构造函数3
- 《unix网络编程》(14)使用select、shutdown的客户服务器程序
- mysql 数据表中查找重复记录
- 第4周项目4——指向学生类的指针
- (others)一些常见的帧格式
- 暴力之整除的应用
- 第四周 项目1-三角形构造函数4
- 删除Linux中的特殊符号文件及目录
- 超级牛人在华为工作十年的感悟
- smith 数