socket网络编程是每个服务端开发人员必会技能,但是目前市面上各种web服务器容器,屏蔽了很多底层实现,导致很多socket通信细节被屏蔽,本文结合在linux下C语言socket通信说明一下网络通信的一些注意点。
目录
1.多进程模型tcp服务器
2.参数SO_REUSEADDR使用
3.参数SO_REUSEPORT使用
4.nginx中参数SO_REUSEADDR和SO_REUSEPORT的使用
1.多进程模型tcp服务器
以下代码在主进程中bind,listen监听套接字,然后fork两个子进程,子进程中进行accept等待客户端连接,主进程关闭监听套接字。子进程中将接收到客户端的信息打印并返回给客户端。客户端使用网络测试工具nc。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <ctype.h>
#include <signal.h>int main() {int port = 8090;int lfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == lfd) {perror("scoket");return -1;}int ret = 0;struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = INADDR_ANY;ret = bind(lfd, (const struct sockaddr *)&addr, sizeof(addr));if (-1 == ret) {perror("bind");return -1;}ret = listen(lfd, 128);char buf[8192] = {0};int len = 0;int total = 0;char tmpBuf[1024] = {0};struct sockaddr_in cliaddr; socklen_t socketlen =sizeof(cliaddr);pid_t pro_cli;signal(SIGCHLD, SIG_IGN);std::cout << "main process: " << getpid() << std::endl;for (int i = 0; i < 2; i++) {pro_cli = fork();if (pro_cli > 0) {std::cout << "child process: " << pro_cli << std::endl;}if (0 == pro_cli) {int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &socketlen);std::cout << "process::" << getpid() << " client: " << inet_ntoa(cliaddr.sin_addr) << ":" << ntohs(cliaddr.sin_port) << std::endl;while (true) {if (-1 == cfd) {perror("accept");return -1;}memset(buf, 0x00, sizeof(buf));memset(tmpBuf, 0x00, sizeof(tmpBuf));total = 0;if ((len = recv(cfd, tmpBuf, sizeof(tmpBuf), 0)) > 0) {if (total + len < sizeof(buf)) {memcpy(buf + total, tmpBuf, len);}send(cfd, buf, len, 0);}if ('\0' != buf[0]) {std::cout << buf << std::endl;}if (-1 == len && errno == EAGAIN && total > 0) { //接收数据完毕//}else if (0 == len) { //客户端断开了连接std::cout << "close client" << std::endl;close(cfd);}else {//perror("recv");}}close(cfd);close(lfd);exit(0);}}close(lfd);sleep(999999999); std::cout << "main end" << std::endl;return 0;
}
运行效果如下:
说明:如上显示创建三个进程,主进程17973,两个子进程17974和17975。开启两个nc客户端去连接服务器,并分别发送信息给服务器,服务器回显nc发送的消息。
关于close_wait状态:
(1)只会出现在被动关闭端
(2)状态一直停留在close_wait状态,而没有发送FIN信号并迁移到LAST_ACK状态,一般都是在收到主动关闭发起方的FIN信号后没有调用进程的close方法导致状态不迁移
(3)close_wait状态是不正常的,一般是由于bug产生,应该避免
2.参数SO_REUSEADDR使用
ctrl + C杀死服务器进程时,再重启服务端发生地址已经使用错误。如下所示
服务端是主动关闭的一方,其处于FIN_WAIT2状态,再关闭nc客户端后,其状态为TIME_WAIT,所以服务端不能在此启动。
如何能让服务端进程退出后立马能够在此监听端口呢?
int opt = -1;
int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
完整代码如下所示:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <ctype.h>
#include <signal.h>int main() {int port = 8090;int lfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == lfd) {perror("scoket");return -1;}int ret = 0;int opt = -1;ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = INADDR_ANY;ret = bind(lfd, (const struct sockaddr *)&addr, sizeof(addr));if (-1 == ret) {perror("bind");return -1;}ret = listen(lfd, 128);char buf[8192] = {0};int len = 0;int total = 0;char tmpBuf[1024] = {0};struct sockaddr_in cliaddr; socklen_t socketlen =sizeof(cliaddr);pid_t pro_cli;signal(SIGCHLD, SIG_IGN);std::cout << "main process: " << getpid() << std::endl;for (int i = 0; i < 2; i++) {pro_cli = fork();if (pro_cli > 0) {std::cout << "child process: " << pro_cli << std::endl;}if (0 == pro_cli) {int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &socketlen);std::cout << "process::" << getpid() << " client: " << inet_ntoa(cliaddr.sin_addr) << ":" << ntohs(cliaddr.sin_port) << std::endl;while (true) {if (-1 == cfd) {perror("accept");return -1;}memset(buf, 0x00, sizeof(buf));memset(tmpBuf, 0x00, sizeof(tmpBuf));total = 0;if ((len = recv(cfd, tmpBuf, sizeof(tmpBuf), 0)) > 0) {if (total + len < sizeof(buf)) {memcpy(buf + total, tmpBuf, len);}send(cfd, buf, len, 0);}if ('\0' != buf[0]) {std::cout << buf << std::endl;}if (-1 == len && errno == EAGAIN && total > 0) { //接收数据完毕//}else if (0 == len) { //客户端断开了连接std::cout << "close client" << std::endl;close(cfd);}else {//perror("recv");}}close(cfd);close(lfd);exit(0);}}close(lfd);sleep(999999999); std::cout << "main end" << std::endl;return 0;
}
3.参数SO_REUSEPORT使用
如上所示,端口处于LISTEN状态,再其动一个服务端程序,出现地址已经使用错误,如何在端口已经监听的情况下再次监听端口呢?
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
完整代码如下:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <assert.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <ctype.h>
#include <signal.h>int main() {int port = 8090;int lfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == lfd) {perror("scoket");return -1;}int ret = 0;int opt = -1;ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));if (-1 == ret) {perror("setsockopt");return -1;}setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));if (-1 == ret) {perror("setsockopt");return -1;}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = INADDR_ANY;ret = bind(lfd, (const struct sockaddr *)&addr, sizeof(addr));if (-1 == ret) {perror("bind");return -1;}ret = listen(lfd, 128);char buf[8192] = {0};int len = 0;int total = 0;char tmpBuf[1024] = {0};struct sockaddr_in cliaddr; socklen_t socketlen =sizeof(cliaddr);pid_t pro_cli;signal(SIGCHLD, SIG_IGN);std::cout << "main process: " << getpid() << std::endl;for (int i = 0; i < 2; i++) {pro_cli = fork();if (pro_cli > 0) {std::cout << "child process: " << pro_cli << std::endl;}if (0 == pro_cli) {int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &socketlen);std::cout << "process::" << getpid() << " client: " << inet_ntoa(cliaddr.sin_addr) << ":" << ntohs(cliaddr.sin_port) << std::endl;while (true) {if (-1 == cfd) {perror("accept");return -1;}memset(buf, 0x00, sizeof(buf));memset(tmpBuf, 0x00, sizeof(tmpBuf));total = 0;if ((len = recv(cfd, tmpBuf, sizeof(tmpBuf), 0)) > 0) {if (total + len < sizeof(buf)) {memcpy(buf + total, tmpBuf, len);}send(cfd, buf, len, 0);}if ('\0' != buf[0]) {std::cout << buf << std::endl;}if (-1 == len && errno == EAGAIN && total > 0) { //接收数据完毕//}else if (0 == len) { //客户端断开了连接std::cout << "close client" << std::endl;close(cfd);}else {//perror("recv");}}close(cfd);close(lfd);exit(0);}}close(lfd);sleep(999999999); std::cout << "main end" << std::endl;return 0;
}
网络状态和运行状态如下:
通过运行看,不同进程可以监听相同的端口
4.nginx中参数SO_REUSEADDR和SO_REUSEPORT的使用