Socket端口复用
先说为什么要使用socket端口复用?如果你遇到过这样的问题:server程序重启之后,无法连接,需要过一段时间才能连接上?
1.一个监听(listen)server已经启动
2.当有client有连接请求的时候,server产生一个子进程去处理该client的事物.
3.server主进程终止了,但是子进程还在占用该连接处理client的事情.虽然子进程终止了,但是由于子进程没有终止,该socket的引用计数不会为0,所以该socket不会被关闭.
4.server程序重启。
这个时候由于端口已经被占用,所以无法重新再bind,这也使得TIME_WAIT状态设计的原因之一。
- int getsockopt(int sockfd, int level, int optname,
- void *optval, socklen_t *optlen);
- int setsockopt(int sockfd, int level, int optname,
- const void *optval, socklen_t optlen);
- int setsockopt(
- SOCKET s,
- int level,
- int optname,
- const char* optval,
- int optlen
- );
level:(级别): 指定选项代码的类型。
SOL_SOCKET: 基本套接口
IPPROTO_IP: IPv4套接口
IPPROTO_IPV6: IPv6套接口
IPPROTO_TCP: TCP套接口
optname(选项名): 选项名称
optval(选项值): 是一个指向变量的指针 类型:整形,套接口结构, 其他结构类型:linger{}, timeval{ }
optlen(选项长度) :optval 的大小
在bind之前添加源码,支持端口复用:
- int on = 1;
- if (setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,
- &on,sizeof(on)) == -1)
- err_exit("setsockopt SO_REUSEADDR error");
首先,一个端口肯定只能绑定一个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
处理多客户连接:
- void echo(int clientfd);
- int main()
- {
- int listenfd = socket(AF_INET, SOCK_STREAM, 0);
- if (listenfd == -1)
- ERR_EXIT("socket error");
- int on = 1;
- if (setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,
- &on,sizeof(on)) == -1) //应对重启server,端口复用
- ERR_EXIT("setsockopt SO_REUSEADDR error");
- struct sockaddr_in addr;
- addr.sin_family = AF_INET;
- addr.sin_port = htons(8001);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- if (bind(listenfd, (const struct sockaddr *)&addr, sizeof(addr)) == -1)
- ERR_EXIT("bind error");
- if (listen(listenfd, SOMAXCONN) == -1)
- ERR_EXIT("listen error");
- struct sockaddr_in clientAddr;
- socklen_t addrLen = sizeof(clientAddr);
- while (true)
- {
- int clientfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
- if (clientfd == -1)
- ERR_EXIT("accept error");
- //打印客户IP地址与端口号
- cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)
- << ", " << ntohs(clientAddr.sin_port) << endl;
- pid_t pid = fork();
- if (pid == -1)
- ERR_EXIT("fork error");
- else if (pid > 0)
- close(clientfd);
- //子进程处理链接
- else if (pid == 0)
- {
- close(listenfd);
- echo(clientfd);
- //子进程一定要exit, 否则的话, 该子进程也会回到accept处
- exit(EXIT_SUCCESS);
- }
- }
- close(listenfd);
- }
- void echo(int clientfd)
- {
- char buf[512] = {0};
- int readBytes;
- while ((readBytes = read(clientfd, buf, sizeof(buf))) > 0)
- {
- cout << buf;
- if (write(clientfd, buf, readBytes) == -1)
- ERR_EXIT("write socket error");
- memset(buf, 0, sizeof(buf));
- }
- if (readBytes == 0)
- {
- cerr << "client connect closed..." << endl;
- close(clientfd);
- }
- else if (readBytes == -1)
- ERR_EXIT("read socket error");
- }
server端与client都有两个进程:
(1)父进程负责从socket中读取数据将其写至终端, 由于父进程使用的是read系统调用的阻塞版本, 因此如果socket中没有数据的话, 父进程会一直阻塞; 如果read返回0, 表示对端连接关闭, 则父进程会发送SIGUSR1信号给子进程, 通知其退出;
(2)子进程负责从键盘读取数据将其写入socket, 如果键盘没有数据的话, 则fgets调用会一直阻塞;
- //server端代码
- void sigHandler(int signo)
- {
- cout << "recv a signal = " << signo << endl;
- exit(EXIT_SUCCESS);
- }
- int main()
- {
- int listenfd = socket(AF_INET, SOCK_STREAM, 0);
- if (listenfd == -1)
- ERR_EXIT("socket error");
- int on = 1;
- if (setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,
- &on,sizeof(on)) == -1)
- ERR_EXIT("setsockopt SO_REUSEADDR error");
- struct sockaddr_in addr;
- addr.sin_family = AF_INET;
- addr.sin_port = htons(8001);
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- if (bind(listenfd, (const struct sockaddr *)&addr, sizeof(addr)) == -1)
- ERR_EXIT("bind error");
- if (listen(listenfd, SOMAXCONN) == -1)
- ERR_EXIT("listen error");
- struct sockaddr_in clientAddr;
- socklen_t addrLen = sizeof(clientAddr);
- int clientfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
- if (clientfd == -1)
- ERR_EXIT("accept error");
- close(listenfd);
- //打印客户IP地址与端口号
- cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)
- << ", " << ntohs(clientAddr.sin_port) << endl;
- char buf[512] = {0};
- pid_t pid = fork();
- if (pid == -1)
- ERR_EXIT("fork error");
- //父进程: socket -> terminal
- else if (pid > 0)
- {
- int readBytes;
- while ((readBytes = read(clientfd, buf, sizeof(buf))) > 0)
- {
- cout << buf;
- memset(buf, 0, sizeof(buf));
- }
- if (readBytes == 0)
- cout << "client connect closed...\nserver exiting..." << endl;
- else if (readBytes == -1)
- ERR_EXIT("read socket error");
- //通知子进程退出
- kill(pid, SIGUSR1);
- }
- //子进程: keyboard -> socket
- else if (pid == 0)
- {
- signal(SIGUSR1, sigHandler);
- while (fgets(buf, sizeof(buf), stdin) != NULL)
- {
- if (write(clientfd, buf, strlen(buf)) == -1)
- err_exit("write socket error");
- memset(buf, 0, sizeof(buf));
- }
- }
- close(clientfd);
- exit(EXIT_SUCCESS);
- }
- //client端代码与说明
- int main()
- {
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd == -1)
- ERR_EXIT("socket error");
- //填写服务器端口号与IP地址
- struct sockaddr_in serverAddr;
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(8001);
- serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
- if (connect(sockfd, (const struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1)
- ERR_EXIT("connect error");
- char buf[512] = {0};
- pid_t pid = fork();
- if (pid == -1)
- ERR_EXIT("fork error");
- //父进程: socket -> terminal
- else if (pid > 0)
- {
- int readBytes;
- while ((readBytes = read(sockfd, buf, sizeof(buf))) > 0)
- {
- cout << buf;
- memset(buf, 0, sizeof(buf));
- }
- if (readBytes == 0)
- cout << "server connect closed...\nclient exiting..." << endl;
- else if (readBytes == -1)
- ERR_EXIT("read socket error");
- kill(pid, SIGUSR1);
- }
- //子进程: keyboard -> socket
- else if (pid == 0)
- {
- signal(SIGUSR1, sigHandler);
- while (fgets(buf, sizeof(buf), stdin) != NULL)
- {
- if (write(sockfd, buf, strlen(buf)) == -1)
- ERR_EXIT("write socket error");
- memset(buf, 0, sizeof(buf));
- }
- }
- close(sockfd);
- exit(EXIT_SUCCESS);
- }
Makefile文件
- .PHONY: clean all
- CC = g++
- CPPFLAGS = -Wall -g -pthread -std=c++11
- BIN = server client
- SOURCES = $(BIN.=.cpp)
- all: $(BIN)
- $(BIN): $(SOURCES)
- clean:
- -rm -rf $(BIN) bin/ obj/ core