Linux下的socket编程实践(三)端口复用和 P2P多进程服务器

Socket端口复用

先说为什么要使用socket端口复用?如果你遇到过这样的问题:server程序重启之后,无法连接,需要过一段时间才能连接上?

 1.一个监听(listen)server已经启动
  2.当有client有连接请求的时候,server产生一个子进程去处理该client的事物.
3.server主进程终止了,但是子进程还在占用该连接处理client的事情.虽然子进程终止了,但是由于子进程没有终止,该socket的引用计数不会为0,所以该socket不会被关闭.
4.server程序重启。

这个时候由于端口已经被占用,所以无法重新再bind,这也使得TIME_WAIT状态设计的原因之一。

[cpp]  view plain copy
  1. int getsockopt(int sockfd, int level, int optname,    
  2.                void *optval, socklen_t *optlen);    
  3. int setsockopt(int sockfd, int level, int optname,    
  4.                const void *optval, socklen_t optlen);    
SO_REUSEADDR允许同一个端口上绑定多个IP,只要这些IP不同。服务端尽可能使用SO_REUSEADDR,在绑定之前尽可能调用setsockopt来设置SO_REUSEADDR套接字选项。该选项可以使得server不必等待TIME_WAIT状态消失。

[cpp]  view plain copy
  1. int setsockopt(  
  2.     SOCKET s,  
  3.     int level,  
  4.     int optname,  
  5.     const char* optval,  
  6.     int optlen  
  7. );  
s(套接字): 指向一个打开的套接口描述字
level:(级别): 指定选项代码的类型。
SOL_SOCKET: 基本套接口
IPPROTO_IP: IPv4套接口
IPPROTO_IPV6: IPv6套接口
IPPROTO_TCP: TCP套接口
optname(选项名): 选项名称
optval(选项值): 是一个指向变量的指针 类型:整形,套接口结构, 其他结构类型:linger{}, timeval{ }
optlen(选项长度) :optval 的大小
在bind之前添加源码,支持端口复用:

[cpp]  view plain copy
  1. int on = 1;    
  2. if (setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,    
  3.                &on,sizeof(on)) == -1)    
  4.     err_exit("setsockopt SO_REUSEADDR error");    
   另外,我想到一个问题:因为SO_REUSEADDR是为了应对重启server使用的,那么未使用选项的服务端在accept之后产生一个新的socket连接,那么这个连接真的会分配一个新的端口吗?当我实践之后却发现这些新的socket连接和bind的端口是一致的!!难道一个端口可以绑定多个套接字吗,当然不是,下面是分析:

   首先,一个端口肯定只能绑定一个socket。我认为,服务器端的端口在bind的时候已经绑定到了监听套接字socetfd所描述的对象上,accept函数新创建的socket对象其实并没有进行端口的占有,而是复制了socetfd的本地IP和端口号,并且记录了连接过来的客户端的IP和端口号。

   那么,当客户端发送数据过来的时候,究竟是与哪一个socket对象通信呢?

   客户端发送过来的数据可以分为2种,一种是连接请求,一种是已经建立好连接后的数据传输。

   由于TCP/IP协议栈是维护着一个接收和发送缓冲区的。在接收到来自客户端的数据包后,服务器端的TCP/IP协议栈应该会做如下处理:如果收到的是请求连接的数据包,则传给监听着连接请求端口的socetfd套接字,进行accept处理;如果是已经建立过连接后的客户端数据包,则将数据放入接收缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用socketfd_new 套接字通过recv或者read函数到缓冲区里面去取指定的数据(因为socketfd_new代表的socket对象记录了客户端IP和端口,因此可以鉴别)。

   在解决这个问题的时候,参考了博客  http://ticktick.blog.51cto.com/823160/779866


处理多客户连接:

[cpp]  view plain copy
  1. void echo(int clientfd);    
  2. int main()    
  3. {    
  4.     int listenfd = socket(AF_INET, SOCK_STREAM, 0);    
  5.     if (listenfd == -1)    
  6.         ERR_EXIT("socket error");    
  7.     int on = 1;    
  8.     if (setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,    
  9.                    &on,sizeof(on)) == -1)  //应对重启server,端口复用   
  10.         ERR_EXIT("setsockopt SO_REUSEADDR error");    
  11.     
  12.     struct sockaddr_in addr;    
  13.     addr.sin_family = AF_INET;    
  14.     addr.sin_port = htons(8001);    
  15.     addr.sin_addr.s_addr = htonl(INADDR_ANY);    
  16.     if (bind(listenfd, (const struct sockaddr *)&addr, sizeof(addr)) == -1)    
  17.         ERR_EXIT("bind error");    
  18.     if (listen(listenfd, SOMAXCONN) == -1)    
  19.         ERR_EXIT("listen error");    
  20.     
  21.     struct sockaddr_in clientAddr;    
  22.      
  23.     socklen_t addrLen = sizeof(clientAddr);    
  24.     while (true)    
  25.     {    
  26.         int clientfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);    
  27.         if (clientfd == -1)    
  28.             ERR_EXIT("accept error");    
  29.         //打印客户IP地址与端口号    
  30.         cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)    
  31.              << ", " << ntohs(clientAddr.sin_port) << endl;    
  32.     
  33.         pid_t pid = fork();    
  34.         if (pid == -1)    
  35.             ERR_EXIT("fork error");    
  36.         else if (pid > 0)    
  37.             close(clientfd);    
  38.         //子进程处理链接    
  39.         else if (pid == 0)    
  40.         {    
  41.             close(listenfd);    
  42.             echo(clientfd);    
  43.             //子进程一定要exit, 否则的话, 该子进程也会回到accept处    
  44.             exit(EXIT_SUCCESS);    
  45.         }    
  46.     }    
  47.     close(listenfd);    
  48. }    
  49. void echo(int clientfd)    
  50. {    
  51.     char buf[512] = {0};    
  52.     int readBytes;    
  53.     while ((readBytes = read(clientfd, buf, sizeof(buf))) > 0)    
  54.     {    
  55.         cout << buf;    
  56.         if (write(clientfd, buf, readBytes) == -1)    
  57.             ERR_EXIT("write socket error");    
  58.         memset(buf, 0, sizeof(buf));    
  59.     }    
  60.     if (readBytes == 0)    
  61.     {    
  62.         cerr << "client connect closed..." << endl;    
  63.         close(clientfd);    
  64.     }    
  65.     else if (readBytes == -1)    
  66.         ERR_EXIT("read socket error");    
  67. }    
简单的P2P聊天程序的实现

server端与client都有两个进程:

 (1)父进程负责从socket中读取数据将其写至终端, 由于父进程使用的是read系统调用的阻塞版本, 因此如果socket中没有数据的话, 父进程会一直阻塞; 如果read返回0, 表示对端连接关闭, 则父进程会发送SIGUSR1信号给子进程, 通知其退出;

 (2)子进程负责从键盘读取数据将其写入socket, 如果键盘没有数据的话, 则fgets调用会一直阻塞;

[cpp]  view plain copy
  1. //server端代码  
  2. void sigHandler(int signo)  
  3. {  
  4.     cout << "recv a signal = " << signo << endl;  
  5.     exit(EXIT_SUCCESS);  
  6. }  
  7. int main()    
  8. {    
  9.     int listenfd = socket(AF_INET, SOCK_STREAM, 0);    
  10.     if (listenfd == -1)    
  11.         ERR_EXIT("socket error");    
  12.     int on = 1;    
  13.     if (setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,    
  14.                    &on,sizeof(on)) == -1)    
  15.         ERR_EXIT("setsockopt SO_REUSEADDR error");    
  16.     
  17.     struct sockaddr_in addr;    
  18.     addr.sin_family = AF_INET;    
  19.     addr.sin_port = htons(8001);    
  20.     addr.sin_addr.s_addr = htonl(INADDR_ANY);    
  21.     if (bind(listenfd, (const struct sockaddr *)&addr, sizeof(addr)) == -1)    
  22.         ERR_EXIT("bind error");    
  23.     if (listen(listenfd, SOMAXCONN) == -1)    
  24.         ERR_EXIT("listen error");    
  25.     
  26.     struct sockaddr_in clientAddr;    
  27.     socklen_t addrLen = sizeof(clientAddr);    
  28.     int clientfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);    
  29.     if (clientfd == -1)    
  30.         ERR_EXIT("accept error");    
  31.     close(listenfd);    
  32.     //打印客户IP地址与端口号    
  33.     cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)    
  34.          << ", " << ntohs(clientAddr.sin_port) << endl;    
  35.     
  36.     char buf[512] = {0};    
  37.     pid_t pid = fork();    
  38.     if (pid == -1)    
  39.         ERR_EXIT("fork error");    
  40.     //父进程: socket -> terminal    
  41.     else if (pid > 0)    
  42.     {    
  43.         int readBytes;    
  44.         while ((readBytes = read(clientfd, buf, sizeof(buf))) > 0)    
  45.         {    
  46.             cout << buf;    
  47.             memset(buf, 0, sizeof(buf));    
  48.         }    
  49.         if (readBytes == 0)    
  50.             cout << "client connect closed...\nserver exiting..." << endl;    
  51.         else if (readBytes == -1)    
  52.             ERR_EXIT("read socket error");    
  53.         //通知子进程退出    
  54.         kill(pid, SIGUSR1);    
  55.     }    
  56.     //子进程: keyboard -> socket    
  57.     else if (pid == 0)    
  58.     {    
  59.         signal(SIGUSR1, sigHandler);    
  60.         while (fgets(buf, sizeof(buf), stdin) != NULL)    
  61.         {    
  62.             if (write(clientfd, buf, strlen(buf)) == -1)    
  63.                 err_exit("write socket error");    
  64.             memset(buf, 0, sizeof(buf));    
  65.         }    
  66.     }    
  67.     close(clientfd);    
  68.     exit(EXIT_SUCCESS);    
  69. }    

[cpp]  view plain copy
  1. //client端代码与说明    
  2. int main()    
  3. {    
  4.     int sockfd = socket(AF_INET, SOCK_STREAM, 0);    
  5.     if (sockfd == -1)    
  6.         ERR_EXIT("socket error");    
  7.     
  8.     //填写服务器端口号与IP地址    
  9.     struct sockaddr_in serverAddr;    
  10.     serverAddr.sin_family = AF_INET;    
  11.     serverAddr.sin_port = htons(8001);    
  12.     serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");    
  13.     if (connect(sockfd, (const struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)    
  14.         ERR_EXIT("connect error");    
  15.     
  16.     char buf[512] = {0};    
  17.     pid_t pid = fork();    
  18.     if (pid == -1)    
  19.         ERR_EXIT("fork error");    
  20.     //父进程: socket -> terminal    
  21.     else if (pid > 0)    
  22.     {    
  23.         int readBytes;    
  24.         while ((readBytes = read(sockfd, buf, sizeof(buf))) > 0)    
  25.         {    
  26.             cout << buf;    
  27.             memset(buf, 0, sizeof(buf));    
  28.         }    
  29.         if (readBytes == 0)    
  30.             cout << "server connect closed...\nclient exiting..." << endl;    
  31.         else if (readBytes == -1)    
  32.             ERR_EXIT("read socket error");    
  33.         kill(pid, SIGUSR1);    
  34.     }    
  35.     //子进程: keyboard -> socket    
  36.     else if (pid == 0)    
  37.     {    
  38.         signal(SIGUSR1, sigHandler);    
  39.         while (fgets(buf, sizeof(buf), stdin) != NULL)    
  40.         {    
  41.             if (write(sockfd, buf, strlen(buf)) == -1)    
  42.                 ERR_EXIT("write socket error");    
  43.             memset(buf, 0, sizeof(buf));    
  44.         }    
  45.     }    
  46.     close(sockfd);    
  47.     exit(EXIT_SUCCESS);    
  48. }    

Makefile文件

[cpp]  view plain copy
  1. .PHONY: clean all  
  2. CC = g++  
  3. CPPFLAGS = -Wall -g -pthread -std=c++11  
  4. BIN = server client  
  5. SOURCES = $(BIN.=.cpp)  
  6.   
  7. all: $(BIN)  
  8. $(BIN): $(SOURCES)   
  9.   
  10. clean:  
  11.     -rm -rf $(BIN) bin/ obj/ core  
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
socket编程中的多路复用是指通过一种机制,使一个进程可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读写操作准备就绪),能够通知程序进行相应的读写操作。在Linux中,常用的多路复用机制有select、poll和epoll。其中,select是最古老的多路复用机制,poll是select的改进版,而epoll是最新、最高效的多路复用机制。多路复用机制可以大大提高程序的并发性能,使得程序可以同时处理多个客户端请求。 下面是一个简单的使用select实现多路复用的流程图和代码示例: 流程图: ``` 1. 创建socket并绑定端口 2. 将socket设置为非阻塞模式 3. 创建fd_set集合,并将socket加入集合 4. 进入循环,调用select函数,等待文件描述符就绪 5. 如果socket就绪,表示有新的客户端连接请求,调用accept函数接受连接 6. 如果其他文件描述符就绪,表示有客户端发送数据,调用recv函数接收数据并处理 7. 回到步骤4,继续等待文件描述符就绪 ``` 代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/select.h> #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int main(int argc, char *argv[]) { int server_fd, client_fd, max_fd, activity, i, valread, sd; struct sockaddr_in address; char buffer[BUFFER_SIZE] = {0}; fd_set readfds; // 创建socket并绑定端口 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(atoi(argv[1])); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } if (listen(server_fd, MAX_CLIENTS) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } // 将socket设置为非阻塞模式 int flags = fcntl(server_fd, F_GETFL, 0); fcntl(server_fd, F_SETFL, flags | O_NONBLOCK); // 创建fd_set集合,并将socket加入集合 FD_ZERO(&readfds); FD_SET(server_fd, &readfds); max_fd = server_fd; // 进入循环,调用select函数,等待文件描述符就绪 while (1) { activity = select(max_fd + 1, &readfds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); exit(EXIT_FAILURE); } // 如果socket就绪,表示有新的客户端连接请求,调用accept函数接受连接 if (FD_ISSET(server_fd, &readfds)) { if ((client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) { perror("accept error"); exit(EXIT_FAILURE); } printf("New connection, socket fd is %d, ip is : %s, port : %d\n", client_fd, inet_ntoa(address.sin_addr), ntohs(address.sin_port)); // 将新的客户端socket加入集合 FD_SET(client_fd, &readfds); if (client_fd > max_fd) { max_fd = client_fd; } } // 如果其他文件描述符就绪,表示有客户端发送数据,调用recv函数接收数据并处理 for (i = server_fd + 1; i <= max_fd; i++) { sd = i; if (FD_ISSET(sd, &readfds)) { if ((valread = recv(sd, buffer, BUFFER_SIZE, 0)) == 0) { // 客户端关闭连接 printf("Client disconnected, socket fd is %d\n", sd); close(sd); FD_CLR(sd, &readfds); } else { // 处理客户端发送的数据 printf("Received message from client, socket fd is %d, message is %s\n", sd, buffer); memset(buffer, 0, BUFFER_SIZE); } } } } return 0; } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值