linux网络编程之socket编程(八)
学习socket编程继续,今天要学习的内容如下:
先来简单介绍一下这五种模型分别是哪些,偏理论,有个大致的印象就成,做个对比,因为最终只会研究一个I/O模型,也是经常会用到的,
阻塞I/O:
先用一个图来描述它:
实际上,之前我们使用的套接口I/O编程都是用该模型,针对上面的图进行说明一下:一旦套接口连接成功之后,就可以recv数据了,如下:
会向系统发起请求来接收数据,而这个recv请求是阻塞的,那什么时候解除阻塞呢,直到对方等方数据过来,填充了recv这个套接口所对应的接收缓冲区,才会解除,也就是说如图:
也就是说,接收缓冲区之前是“没有数据的”,一旦对等方发送数据过来,将接收缓冲区数据填充,这时候就会将数据从内核空间(也就是套接口接收缓冲区)复制到用户空间,如下:
因为我们在recv的时候,会提供一个buffer,如下:
一旦拷贝完成,recv函数就返回了,这时就可以进行数据处理了,如下:
非阻塞I/O【用得很少,做了解既可】:
关于这个模形,由于也没有得到推广,所以这里就略过,我们把重点花在有意义的事上。
对于上面列的五种I/O模型, 接下来主要研究I/O复用模型中的select模型,对于它,上面介绍该模型时也说过,它可以管理多个文件描述符,或者说可以管理多个I/O,
它的函数原形如下:
一旦某个I/O检测到了我们所感兴趣的事件就立刻返回,如果有多个I/O,当发生事件时,将返回到的一些事件,填充到对应的集合当中(readfds),并且返回一个事件的个数,这时候就可以轮循事件,一个个处理它,而这时候的事件是不会阻塞的,因为select已经提前阻塞了,它的返回意味着事件已经到来了,为了更好的理解select的用法,我们首先来看一下上次我们所实现的回射客户/服务器程序具有什么样的问题,来做一个回顾,然后再用select函数来解决这个问题:
这时候,将服务端关才掉,这里用kill命令来模拟:
这时再来查看下状态:
而根据TCP关闭连接的状态来看:
如果客户端read返回为0时,则应该会调close,进而服务端最终状态为TIME_WAIT状态,为啥没有进入此状态呢?对于这个,博文:http://www.cnblogs.com/webor2006/p/4023138.html 也对其进行了详细的解释,简略的说就是由于客户端阻塞在这个位置了:
本质的原因是从键盘接收数据跟网络接收数据这两个事件没有办法同时进行处理,这时可以用select来进行管理,管理fgets标准输入的I/O和sock套接口I/O,一旦其中一个或者多个产生可读事件,则进行处理,也就是这个时候,既可以在接收键盘数据的同时,也可以检测到网络数据的到来,这时就可以进行相应的处理,因为select函数能够解除阻塞,所以,接下来,利用它来改进回射客户端程序。
理解参数:
select可以看成是一个管理者,可以管理多个I/O,一旦其中的一个I/0检测到我们感兴趣的事件,select函数就返回,返回值为检测到的事件个数,并且返回哪些I/O发生了事件,遍历这些事件,进而处理事件,根据这些理论,对于其函数的参数就比较容易理解了:
①、fd_set *readfds:这时一个集合,表示一个读的集合,也是最常用的一个集合,表示如果检测到有读的套接口则放到这个集合中,一旦数据可读,select就可以返回。
②、fd_set *writefds【这次学习先用不上,可以直接填空】:这个从单词上来看,就很容易理解,表示可写的集合。
③、fd_set *exceptfds【这次学习先用不上,可以直接填空】:异常的集合。
④、struct timeval *timeout:这表示超时时间,如果填写NULL,则不会超时,一定要检测到事件后才会返回;如果指定是超时时间,则在超时时间到来的时候还没有检测到事件,也会返回,这时返回的事件个数就等于0,另外select返回失败为-1。
⑤、nfds:它表示存放在集合中(readfds、writefds、exceptfds)的这些描述符的最大值+1,比如:readfds集合中存放了描述符3、5、8,而writefds集合中存放了描述符4、9,那么这个参数就是集合中最大描述符9+1=10。
另对,对于返回值,是返回哪些I/O发生了事件,这是什么意思呢,假如readfds集合提交了3、4、5,我要关心这三个I/O的可读事件,这时如果3跟5发生了可读事件,我如何标识它呢?实际上就是将readfds这个集合改变,集合的内容改成了3、5,这是返回的准备到的个数为2,用图来表示如下:
从图中描述可以看出,readfds是输出输出参数,同理,writefds、exceptfds、timeout也是输出输出参数(比如指定的是2s的时间,但是1s内就返回了,这时后它的值就为剩余的时间)。
与select这些集合操作相配合的有四个宏进行操作,下面来简述一下,之后都会用到:
将文件描述符fd从集合set当中移除。
判定文件描述符fd是否在集合set中,注意:这里的set不是输入输出参数,也就是只读的。
将文件描述符fd添加到集合set当中。
清空集合。
好了,下面就用上面的一些理论来改进程序,来更好的理解select函数的用法,对于之前的代码,只需要对客户端这个函数的实现进行改进,如下:
首先先将函数的实现注释掉,然后一步步用select来改造,根据select的参数来编写:
第一步,首先获得最大的文件描述符,也是第一个填充第一个参数:
另外可以将sock和stdin两个文件描述符加入到集合中:
接下来,由于事件成功返回了,那就可以判断标准输入fd_stdin、sock是否在rset集合里,如果在集合中就证明已经检测到了事件,然后就可以分别进行判断处理了:
对于套接口产生事件,应该将原来的代码挪进来:
实现如下:
对于键盘的输入事件,也应该将原来的代码挪过来,如下:
具体代码如下:
下面编译运行看下之前的问题有没有解决:
这些状态都比较好理解,下面到了关键验证步骤,就是先将服务端关闭掉:
从中可以看到,服务端变为了TIME_WAIT状态,也就是向前进常迈进了,这次就不会因为fgets阻塞造成无法进入此正常状态了,而客户端这时就变为CLOSED状态了,当然通过命令就看不到此状态了,如下:
最后再贴一下经过改用select修改的客户服务回射的完整代码如下:
echosrv.c:
#include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) ssize_t readn(int fd, void *buf, size_t count) { size_t nleft = count; ssize_t nread; char *bufp = (char*)buf; while (nleft > 0) { if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nread == 0) return count - nleft; bufp += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nwritten == 0) continue; bufp += nwritten; nleft -= nwritten; } return count; } ssize_t recv_peek(int sockfd, void *buf, size_t len) { while (1) { int ret = recv(sockfd, buf, len, MSG_PEEK); if (ret == -1 && errno == EINTR) continue; return ret; } } ssize_t readline(int sockfd, void *buf, size_t maxline) { int ret; int nread; char *bufp = buf; int nleft = maxline; while (1) { ret = recv_peek(sockfd, bufp, nleft); if (ret < 0) return ret; else if (ret == 0) return ret; nread = ret; int i; for (i=0; i<nread; i++) { if (bufp[i] == '\n') { ret = readn(sockfd, bufp, i+1); if (ret != i+1) exit(EXIT_FAILURE); return ret; } } if (nread > nleft) exit(EXIT_FAILURE); nleft -= nread; ret = readn(sockfd, bufp, nread); if (ret != nread) exit(EXIT_FAILURE); bufp += nread; } return -1; } void echo_srv(int conn) { char recvbuf[1024]; while (1) { memset(recvbuf, 0, sizeof(recvbuf)); int ret = readline(conn, recvbuf, 1024); if (ret == -1) ERR_EXIT("readline"); if (ret == 0) { printf("client close\n"); break; } fputs(recvbuf, stdout); writen(conn, recvbuf, strlen(recvbuf)); } } void handle_sigchld(int sig) { /* wait(NULL);*/ while (waitpid(-1, NULL, WNOHANG) > 0) ; } int main(void) { /* signal(SIGCHLD, SIG_IGN);*/ signal(SIGCHLD, handle_sigchld); int listenfd; if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) /* if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/ ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/ /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/ int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); if (listen(listenfd, SOMAXCONN) < 0) ERR_EXIT("listen"); struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn; pid_t pid; while (1) { if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0) ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); pid = fork(); if (pid == -1) ERR_EXIT("fork"); if (pid == 0) { close(listenfd); echo_srv(conn); exit(EXIT_SUCCESS); } else close(conn); } return 0; }
echocli.c:
#include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) ssize_t readn(int fd, void *buf, size_t count) { size_t nleft = count; ssize_t nread; char *bufp = (char*)buf; while (nleft > 0) { if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nread == 0) return count - nleft; bufp += nread; nleft -= nread; } return count; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nwritten == 0) continue; bufp += nwritten; nleft -= nwritten; } return count; } ssize_t recv_peek(int sockfd, void *buf, size_t len) { while (1) { int ret = recv(sockfd, buf, len, MSG_PEEK); if (ret == -1 && errno == EINTR) continue; return ret; } } ssize_t readline(int sockfd, void *buf, size_t maxline) { int ret; int nread; char *bufp = buf; int nleft = maxline; while (1) { ret = recv_peek(sockfd, bufp, nleft); if (ret < 0) return ret; else if (ret == 0) return ret; nread = ret; int i; for (i=0; i<nread; i++) { if (bufp[i] == '\n') { ret = readn(sockfd, bufp, i+1); if (ret != i+1) exit(EXIT_FAILURE); return ret; } } if (nread > nleft) exit(EXIT_FAILURE); nleft -= nread; ret = readn(sockfd, bufp, nread); if (ret != nread) exit(EXIT_FAILURE); bufp += nread; } return -1; } void echo_cli(int sock) { /* char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { writen(sock, sendbuf, strlen(sendbuf)); int ret = readline(sock, recvbuf, sizeof(recvbuf)); if (ret == -1) ERR_EXIT("readline"); else if (ret == 0) { printf("client close\n"); break; } fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); memset(recvbuf, 0, sizeof(recvbuf)); } close(sock); */ fd_set rset;//声明一个可读的集合 FD_ZERO(&rset);//将集合清空 int nready;//检测到的事件个数 //获得最大的文件描述符 int maxfd; int fd_stdin = fileno(stdin); if (fd_stdin > sock) maxfd = fd_stdin; else maxfd = sock; char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (1) { FD_SET(fd_stdin, &rset); FD_SET(sock, &rset); nready = select(maxfd+1, &rset, NULL, NULL, NULL); if (nready == -1) ERR_EXIT("select"); if (nready == 0) continue; if (FD_ISSET(sock, &rset)) {//套接口产生了事件 int ret = readline(sock, recvbuf, sizeof(recvbuf)); if (ret == -1) ERR_EXIT("readline"); else if (ret == 0) { printf("server close\n"); break; } fputs(recvbuf, stdout); memset(recvbuf, 0, sizeof(recvbuf)); } if (FD_ISSET(fd_stdin, &rset)) {//标准输入产生的事件 if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) break; writen(sock, sendbuf, strlen(sendbuf)); memset(sendbuf, 0, sizeof(sendbuf)); } } close(sock); } void handle_sigpipe(int sig) { printf("recv a sig=%d\n", sig); } int main(void) { /* signal(SIGPIPE, handle_sigpipe); */ signal(SIGPIPE, SIG_IGN); int sock; if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect"); struct sockaddr_in localaddr; socklen_t addrlen = sizeof(localaddr); if (getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0) ERR_EXIT("getsockname"); printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); echo_cli(sock); return 0; }
好了,夜幕降临了,得准备入睡,明天还得上班,下回见~~~