标签:数据通信 复杂 分代 端口 pac eof 客户端 共享资源 include
前文列出的代码给大家展示了一个最简单的网络程序,但正如文章末尾所提的,这个最简单的网络程序最大的缺点是服务端一次只能服务一个客户端,就比如说你去吃饭,饭店只有一个服务员, 而且服务员在客户离开之前只能为一个客户服务,也就是说你只能等待你前面的客户吃好饭离开了,然后你才能进去吃饭,而在你吃饭的时候时候,你后面来的人都得等你吃完饭才能轮到你后面一个人吃饭。这种模式的缺点很明显,因为在你进去点好菜到买单前的这段时间,这个服务员都是空闲的,为什么不让服务员在这个空闲时间让其他客户进来服务员为他点菜呢?在网络编程中,这个思想是类似的,前面的代码服务端与客户端建立连接后,服务端都在为这个客户端服务,而有可能这个客户端在当时正在等待用户输入,而服务端在等待客户端数据的到达,有什么办法可以让服务端在这个等待的时间服务其他客户端呢?答案是肯定的。解决这个问题的思路有两个:
1、服务端分配多个线程/进程来处理客户端连接,一个客户端对应一个线程/进程
2、服务端的一个进程/线程对应多个连接.
仍然用到饭店吃饭来理解这个思路,思路一是餐厅招聘多个服务员,一个服务员服务一个客户,每来一个客户分配一个服务员专门为他服务。思路二是一个服务员服务多个客户,服务员可以给一个客户点好菜后给另一个客户买单,再给另外一个客户上菜。显而易见,思路一给餐厅增加了成本,来一个客户就需要一个服务员为他服务,当服务员人数既定的情况下就只能服务有限的客户,而思路二在既定服务员人数的情况下却可以进来多的服务客户。
下面分别本篇文章和下篇文章来分别介绍思路一和思路二
#include <netinet/in.h> #include <sys/socket.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <pthread.h> #include <netdb.h> void* doRead(void* arg) { long connfd = (long)arg; char recvBuf[101] = ""; int n = 0; while((n = recv(connfd,recvBuf, sizeof(recvBuf),0 )) > 0) { printf("number of receive bytes = %d.\n", n); //发送数据 send(connfd, recvBuf, n, 0); char* bufTmp = recvBuf; while(*bufTmp != ‘\r‘ && *bufTmp !=‘\n‘) bufTmp++; *bufTmp = ‘\0‘; if(strcmp(recvBuf, "quit") == 0) { break; } } close(connfd); return NULL; } int main() { struct sockaddr_in sockaddr; pthread_t thread_id; int one =1; int ret = 0; bzero(&sockaddr, sizeof(sockaddr)); sockaddr.sin_family = AF_INET; sockaddr.sin_port = htons(8080); sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); long clientfd; int listenfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个socket //将该套接字的绑定端口设为可重用 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*) &one,(socklen_t)sizeof(one)); if(bind(listenfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0) { perror("bind error"); return -1; } ret = listen(listenfd, 16); if(ret < 0) { perror("listen failed.\n"); return -1; } struct sockaddr_in clientAdd; socklen_t len = sizeof(clientAdd); while(1) { clientfd = accept(listenfd, (struct sockaddr *)&clientAdd, &len); if(clientfd < 0) { perror("accept this time"); continue; } if(clientfd > 0) { //由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,值传递 pthread_create(&thread_id, NULL, doRead, (void *)clientfd); printf("thread %d created.\n",thread_id ); pthread_detach(thread_id); } } close(listenfd); return 0; }程序每当客户端发起一个连接时,服务端接受客户端连接accpet返回成功后,都创建一个线程,并将返回的客户端的连接描述符fd传递给线程处理函数,在线程的处理函数doRead处理客户端数据的收发。需要注意的是pthread_create传递的第四个参数传递的是将long类型的数据强转成void *类型的数据,为什么是long类型不是int型呢?这个在没个机器上可能都不一样,在笔者的机器上,int是4个字节,而void*类型是8个字节,所以必须将socket描述符定义成8个字节long类型,否则int型装成long类型编译器报错,此外需要注意的是这里传递的不是clientfd的地址,如果传递了clientfd的地址,那么每当来一个连接的时候,变了clientfd都会被改写,而线程是共享进程的内存空间的,也就是说吐过传递了clienfd的地址所有线程都会使用同一个clienfd,到时其他客户端无法和服务端通信。
#include <netinet/in.h> #include <sys/socket.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> void doread(int fd) { int n = 0; char buf[101] = {0}; while((n = recv(fd, buf, sizeof(buf), 0))> 0) { printf("msg recv is %s\n", buf); send(fd,buf, sizeof(buf), 0); char* bufTmp = buf; while(*bufTmp != ‘\r‘ && *bufTmp !=‘\n‘) bufTmp++; *bufTmp = ‘\0‘; if(strcmp(buf, "quit") == 0) { break; } } } int main() { struct sockaddr_in SerAddr; SerAddr.sin_addr.s_addr = htonl(INADDR_ANY); SerAddr.sin_port = htons(12345); SerAddr.sin_family = AF_INET; int listenfd = socket(AF_INET,SOCK_STREAM,0); if(bind(listenfd, (struct sockaddr*)&SerAddr, sizeof(SerAddr)) < 0) { perror("bind failed"); return -1; } if(listen(listenfd, 64) < 0) { perror("listen failed\n"); return -1; } while(1) { int connfd; struct sockaddr_in clientAddr; socklen_t len = sizeof(clientAddr); connfd = accept(listenfd, (struct sockaddr*)&clientAddr, &len); if(connfd < 0) continue; if(fork() == 0) { close(listenfd); doread(connfd); close(connfd); } close(connfd); }
close(listenfd);
}
从代码中可以看到,父进程创建一个监听socket,并监听来自客户端的请求,每当来一个客户端请求时,创建一个子进程,子进程doread方法处理与客户端的通信。需要注意的是,在子进程创建的开始的时候需要调用close(listenfd),结束时调用close(connfd),而在父进程中调用close(connfd),这是由于父进程和子进程共享资源,而每个文件描述符都有一个引用计数,当fork成功时,listenfd和connfd的引用计数都会变为2,而close函数将判断当前描述符的引用计数,若引用计数为1时才关闭描述符,否则仅将引用计数减1.因此子进程中需要先将listenfd的引用计数减1,,父进程中将connfd的引用计数减1.while(*bufTmp != ‘\r‘ && *bufTmp !=‘\n‘) bufTmp++; *bufTmp = ‘\0‘;那是因为笔者的编程环境是虚拟机+ssh,通过SSH发送的数据发现字符串末尾都带了"\r\n"回车换行字符,因此需要与字符串quit进行比较的时候需要作转换,这部分代码仅用于去掉字符串末尾的‘\r‘ ‘\n\’字符。
标签:数据通信 复杂 分代 端口 pac eof 客户端 共享资源 include
原文地址:http://blog.csdn.net/hailong0715/article/details/68218106