当前位置: 首页 > news >正文

TCP网络编程学习

单进程处理单个客户端

额外知识补充:
端口号:端口号用来识别同一台计算机中进行通信的不同应用程序。
​端口号 是一个 ​16 位整数,范围从 065535。
常见端口号:http:80 ssh:22
知名端口:0~1023 预留给系统或知名服务
注册端口	 1024~49151	分配给用户或企业注册的应用
动态/私有端口 49152~65535	客户端临时使用(操作系统自动分配)INADDR_ANY 内核会自动将套接字绑定到所有可用的网络接口(计算机上的物理和虚拟网卡,通过ipconfig可以查看)。
INADDR_LOOPBACK 仅监听本地回环接口(127.0.0.1)端口复用:端口复用(Port Reuse)是指在同一台机器上,允许多个套接字绑
定到相同的 IP 地址和端口号。(在服务器监听文件描述符绑定端口之前设置)strlen不计算末尾的\0.
read读到数据之后不会在尾部添加\0,可能需要接收端程序主动添加\0
特别注意:addrlen一定要被初始化,否则客户端第一次接入读取的ip和端口号是错的
//第四步:接收客户端连接
sockaddr_in sockaddrClient;
socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的
int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);
//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
read/write:所有文件描述符(文件、管道、套接字等),默认阻塞
recv/send: 仅用于套接字文件描述符,支持更多控制选项
所以套接字的话以后都用send和recv

基础版服务端代码(详细注释)

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
const int PORT = 8080;
int main() {//第一步:创建监听套接字,得到监听文件描述符lfdint lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd < 0) {perror("socket");exit(-1);}//端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)int opt = 1;int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));if (isSetOk == -1) {perror("setsockopt");exit(-1);}//第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口//int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址//if (isTransOk != 1) perror("inet_pton");//sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isBindOk != 0) {perror("bind");exit(-1);}//第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。//如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8if (isListenOk != 0) {perror("listen");exit(-1);}//第四步:接收客户端连接sockaddr_in sockaddrClient;socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的while (1) {int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接if (cfd == -1) {if (errno == EINTR) {std::cout << "EINTR";continue; }perror("accept");exit(-1);}//尝试打印客户端信息,端口号+ip地址std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;//读取客户端信息char buf[1024];while (1) { int readLen = read(cfd, buf, sizeof(buf));if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(cfd);break;//退出与客户端的信息交互} else {perror("read");close(cfd);break;}//读到数据后进行回传const char* message = "this is server:";int writeLen = write(cfd, message,strlen(message));if (writeLen < 0) {perror("write");close(cfd);break;}writeLen = write(cfd, buf,readLen);if (writeLen < 0) {perror("write");close(cfd);break;}}}close(lfd);return 0;
}

基础版客户端代码(详细注释)

//构建客户端
//TCP构建客户端
/*
1.socket,得到监听fd
2.connect建立连接
3.send/write
4.recv/read
7.close
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
const int PORT = 8080;
int main() {//第一步:创建套接字用户通信int cfd = socket(AF_INET, SOCK_STREAM, 0);if (cfd < 0) {perror("socket");exit(-1);}//连接服务器:需要指定连接的服务器ip和端口sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//以这个ip地址访问服务器,其实就是本机的虚拟网卡IPint isConnectOK = connect(cfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isConnectOK == -1) {perror("connect");exit(-1);}char buf[1024];while (1) {if (fgets(buf,sizeof(buf),stdin) == NULL) {perror("fgets");break;} //fgets读完之后buf的末尾会有一个\n需要特别注意一下int writeLen = write(cfd, buf,strlen(buf));if (writeLen < 0) {perror("write");break;}int readLen = read(cfd, buf, sizeof(buf));if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(cfd);break;//退出与客户端的信息交互} else {perror("read");break;}}return 0;
}

多进程服务器代码

涉及一个问题,如何处理在一个进程结束后进行wait(0),因为主进程服务器一直在运行,无法运行wait,因为wait会造成阻塞,主进程服务器需要一直accept接收客户端连接。
使用进程间通信机制:信号
特别注意两个点:
1.wait(&status)阻塞等待任意进程
2.waitpid(pid,&status,options)可以等待回收指定的子进程。pid>0(等待指定的子进程),pid=-1(等待任意子进程),其他的有需要再看。
options选项中:0为阻塞等待,WNOHANG为非阻塞式.WUNTRACED:等待被暂停的子进程。WCONTINUED:等待被继续的子进程。
3.accept在主进程中接收客户端链接,会被信号打断,生成EINTR.这个要特殊处理一下,保证accept跳出后返回。

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
const int PORT = 8080;
//信号处理函数
void signal_handler(int signum) {while(1) {int ret = waitpid(-1, NULL, WNOHANG);//设置非阻塞if(ret == -1) {// 所有的子进程都回收了break;}else if(ret == 0) {// 还有子进程活着break;} else if(ret > 0){// 被回收了printf("子进程 %d 被回收了\n", ret);}}
}
int main() {//添加信号机制处理子进程回收struct sigaction sa;sa.sa_handler = signal_handler;// 设置信号处理函数sigemptyset(&sa.sa_mask); 清空信号掩码sa.sa_flags = 0;                // 默认标志//注册信号捕捉sigaction(SIGCHLD, &sa, NULL);//SIGCHLD子进程结束时,父进程会收到这个信号//第一步:创建监听套接字,得到监听文件描述符lfdint lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd < 0) {perror("socket");exit(-1);}//端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)int opt = 1;int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));if (isSetOk == -1) {perror("setsockopt");exit(-1);}//第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口//int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址//if (isTransOk != 1) perror("inet_pton");//sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isBindOk != 0) {perror("bind");exit(-1);}//第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。//如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8if (isListenOk != 0) {perror("listen");exit(-1);}//第四步:接收客户端连接sockaddr_in sockaddrClient;socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的while (1) {int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接if (cfd == -1) {if (errno == EINTR) {std::cout << "EINTR"<<std::endl;continue; }perror("accept");exit(-1);}//尝试多进程必须要用到exit(0)以及wait(0)int pid = fork();if( pid < 0) {perror("fork");exit(-1);} else if (pid == 0) {//进入子进程//进入子进程,完全继承父进程的所有内存信息//尝试打印客户端信息,端口号+ip地址std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;//读取客户端信息char buf[1024];while (1) { int readLen = read(cfd, buf, sizeof(buf));if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(cfd);break;//退出与客户端的信息交互} else {perror("read");close(cfd);exit(-1);}//读到数据后进行回传const char* message = "this is server:";int writeLen = write(cfd, message,strlen(message));if (writeLen < 0) {perror("write");close(cfd);exit(-1);}writeLen = write(cfd, buf,readLen);if (writeLen < 0) {perror("write");close(cfd);exit(-1);}}exit(0);}}close(lfd);return 0;
}

多线程服务器代码

1.memcpy和直接赋值的区别?
memcpy不考虑数据类型,只考虑源地址和目的地址以及字节数量
直接赋值需要左右两边的数据类型一致,否则可能出现错误

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>//多线程 Compile and link with -pthread
const int PORT = 8080;
//定义结构体存储与客户端连接后的通信cfd;
struct sockInfo {int cfd;sockaddr_in addr;pthread_t tid;
};
sockInfo sockInfos[4];//定义4个线程
//定义线程函数void *(*start_routine) (void *)返回值为void* ,输入为void*的线程函数
void* communicateTackle(void* arg) {//线程函数,和客户端通信//入口参数为accept获取到的信息sockInfo* clientInfo = (sockInfo*)arg;//打印客户端信息in_port_t sinport = clientInfo->addr.sin_port;char clientAddress[INET_ADDRSTRLEN];inet_ntop(AF_INET, &clientInfo->addr.sin_addr.s_addr, clientAddress, sizeof(clientAddress));printf("clientport:%d, clientaddress: %s\n",sinport,clientAddress);//开始和客户端进行沟通int cfd = clientInfo->cfd;char buf[1024];while (1) { int readLen = read(cfd, buf, sizeof(buf));if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(cfd);clientInfo->cfd = -1;break;//退出与客户端的信息交互} else {perror("read");close(cfd);clientInfo->cfd = -1;pthread_exit((void*)-1);}//读到数据后进行回传const char* message = "this is server:";int writeLen = write(cfd, message,strlen(message));if (writeLen < 0) {perror("write");close(cfd);clientInfo->cfd = -1;pthread_exit((void*)-1);}writeLen = write(cfd, buf,readLen);if (writeLen < 0) {perror("write");close(cfd);clientInfo->cfd = -1;pthread_exit((void*)-1);}}    pthread_exit(0);//等价于return NULL//用线程的话,可以分离线程,即父线程不用处理子线程
}int main() {//第一步:创建监听套接字,得到监听文件描述符lfdint lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd < 0) {perror("socket");exit(-1);}//端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)int opt = 1;int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));if (isSetOk == -1) {perror("setsockopt");exit(-1);}//第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口//int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址//if (isTransOk != 1) perror("inet_pton");//sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isBindOk != 0) {perror("bind");exit(-1);}//第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。//如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8if (isListenOk != 0) {perror("listen");exit(-1);}//第四步:接收客户端连接sockaddr_in sockaddrClient;socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的//初始化线程相关信息// 初始化数据int max = sizeof(sockInfos) / sizeof(sockInfos[0]);for(int i = 0; i < max; i++) {bzero(&sockInfos[i], sizeof(sockInfos[i]));//全部置0sockInfos[i].cfd = -1;sockInfos[i].tid = -1;}while (1) {int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接if (cfd == -1) {if (errno == EINTR) {std::cout << "EINTR"<< std::endl;continue; }perror("accept");exit(-1);}//创建线程sockInfo* clientInfo = NULL;for (int i = 0; i < max; i++) {if (sockInfos[i].cfd == -1) {//说明这个结构体可用clientInfo = &sockInfos[i];break;}//处理线程耗尽问题if (i == max - 1) {printf("no vaild thread!!!\n");sleep(1);i=-1;}}clientInfo->cfd = cfd;//为什么使用memcpy?为什么不直接赋值memcpy(&clientInfo->addr,&sockaddrClient,addrlen);int ispcreateOk = pthread_create(&clientInfo->tid, NULL,communicateTackle,clientInfo);if (ispcreateOk !=0) {perror("pthread_create");exit(-1);}//分离线程,线程回收不需要父进程处理,否则需要用pthread_join等待(会造成阻塞影响主进程)pthread_detach(clientInfo->tid);}close(lfd);return 0;
}

select/poll/epoll理解

1.select:最大同时支持1024个连接;每次调用select都要把设定的监听描述符信息fd_set从用户空间搬运到内核空间,再从内核空间搬运到用户空间;
fd_set也不能重用;内核需要遍历指定范围内的文件描述符。
2.poll:支持任意数量连接(数组大小需要提前设置);每次调用 poll 都需要将 struct pollfd 数组从用户空间复制到内核空间,返回时再复制回用户空间;同样内核需要遍历指定范围内的文件描述符。
3.epoll:支持任意数量连接(内核空间操作);文件描述符的管理在内核中完成,减少了用户空间和内核空间之间的数据拷贝。内核通过回调机制通知就绪事件,时间复杂度为 O(1);支持水平和边缘触发模式
4.性能最好的是epoll,不用考虑太多,直接用epoll

API调用:
select:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int  FD_ISSET(int fd, fd_set *set);// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);
poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
epoll:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int 
timeout);

IO多路复用:select

可同时检测的文件描述符数量最大为1024,实际场景中高并发用不上

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/select.h> //selectio多路复用
const int PORT = 8080;
int main() {//第一步:创建监听套接字,得到监听文件描述符lfdint lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd < 0) {perror("socket");exit(-1);}//端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)int opt = 1;int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));if (isSetOk == -1) {perror("setsockopt");exit(-1);}//第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口//int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址//if (isTransOk != 1) perror("inet_pton");//sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isBindOk != 0) {perror("bind");exit(-1);}//第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。//如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8if (isListenOk != 0) {perror("listen");exit(-1);}//第四步:接收客户端连接sockaddr_in sockaddrClient;socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的//定义select相关参数fd_set readfds, readfdsTemp;//fdset是一个结构体,里面包含1024个bit,对应1024个文件描述符FD_ZERO(&readfds);//全部置0FD_SET(lfd, &readfds);//将监听文件描述符假如到readfds列表当中去int maxfd = lfd;//必然是申请的最小的文件描述符(0,1,2是标准输入,标准输出,标准错误,申请文件描述符默认申请的是当前未使用的最小的文件描述符)while (1) {readfdsTemp = readfds;//select IO多路复用,不检测可写与异常,永久阻塞,直到文件描述符发生了变化int changeNum = select(maxfd + 1, &readfdsTemp, NULL,NULL, NULL);if (changeNum == -1) {perror("select"); exit(-1);}else if (changeNum == 0) continue;//返回0是超时,此时设置的是永久阻塞,应该不会出现这个选项else {//开始检测是否有客户端连接(先处理和客户端的连接,再处理和已连接的客户端之间的通信)if (FD_ISSET(lfd,&readfdsTemp)) {//已经有客户端接入了,此时accept不会发生阻塞int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接if (cfd == -1) {if (errno == EINTR) {std::cout << "EINTR";continue; }perror("accept");exit(-1);}//尝试打印新加入的客户端信息,端口号+ip地址std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;FD_SET(cfd,&readfds);maxfd = maxfd > cfd ? maxfd : cfd;}//与新的客户端构成连接//接下来处理和已连接客户端之间的通信for (int i = lfd + 1; i <= maxfd; i++) {if (FD_ISSET(i, &readfdsTemp)) {//与客户端进行通信char buf[1024];int readLen = read(i, buf, sizeof(buf));if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(i);FD_CLR(i,&readfds);continue;} else {perror("read");close(i);FD_CLR(i,&readfds);continue;}//读到数据后进行回传const char* message = "this is server:";int writeLen = write(i, message,strlen(message));if (writeLen < 0) {perror("write");close(i);FD_CLR(i,&readfds);continue;}writeLen = write(i, buf,readLen);if (writeLen < 0) {perror("write");close(i);FD_CLR(i,&readfds);continue;}}}}}close(lfd);return 0;
}

IO多路复用:poll

可同时检测的文件描述符数量可以任意指定
代码中nfds的取值可能有点问题

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <poll.h>
const int PORT = 8080;
#define MAXPOLLNUM 10
int main() {//第一步:创建监听套接字,得到监听文件描述符lfdint lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd < 0) {perror("socket");exit(-1);}//端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)int opt = 1;int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));if (isSetOk == -1) {perror("setsockopt");exit(-1);}//第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口//int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址//if (isTransOk != 1) perror("inet_pton");//sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isBindOk != 0) {perror("bind");exit(-1);}//第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。//如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8if (isListenOk != 0) {perror("listen");exit(-1);}//第四步:接收客户端连接sockaddr_in sockaddrClient;socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的pollfd pollfds[MAXPOLLNUM];//poll的特点:可以指定任意大小,相比select的1024限制更大一些//初始化for (int i = 0; i < MAXPOLLNUM; i++) {pollfds[i].fd = -1;pollfds[i].events = POLLIN;pollfds[i].revents = 0;}pollfds[0].fd = lfd;//监听描述符int nfds = 0;//这个是第一个参数数组中最后一个有效元素的下标,设置的0为当前最后有效元素的下标while (1) {//nfds: 这个是第一个参数数组中最后一个有效元素的下标 + 1int changeNum = poll(pollfds, nfds + 1, -1);//阻塞监听if (changeNum == -1) {perror("poll"); exit(-1);}else if (changeNum == 0) continue;//阻塞一定时间后没有监听到任何描述符,前面-1已经设置了永久阻塞,这一步是不会发生的else {//首先检测是否有客户端连接到来,之后再处理已经连接的客户端通信if (pollfds[0].revents & POLLIN) {//已经有客户端接入了,accept不会阻塞int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的客户端连接,并建立一个新的文件描述符用于和该客户端进行连接if (cfd == -1) {if (errno == EINTR) {std::cout << "EINTR";continue; }perror("accept");exit(-1);}std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;//将新连接加入到监听列表中for (int i = 1; i < MAXPOLLNUM; i++) {if (pollfds[i].fd == -1) {pollfds[i].fd = cfd;pollfds[i].events = POLLIN;pollfds[i].revents = 0;nfds = nfds > cfd ? nfds : cfd;break;}}}//开始处理和已连接客户端的通信for (int i = 1; i < nfds; i++) {if (pollfds[i].revents & POLLIN) {//开始处理通信int cfd = pollfds[i].fd;char buf[1024];int readLen = read(cfd, buf, sizeof(buf));if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(cfd);pollfds[i].fd = -1;continue;} else {perror("read");close(cfd);pollfds[i].fd = -1;continue;}//读到数据后进行回传const char* message = "this is server:";int writeLen = write(cfd, message,strlen(message));if (writeLen < 0) {perror("write");close(cfd);pollfds[i].fd = -1;continue;}writeLen = write(cfd, buf,readLen);if (writeLen < 0) {perror("write");close(cfd);pollfds[i].fd = -1;continue;}}}}}close(lfd);return 0;
}

IO多路复用:epoll

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/epoll.h>
const int PORT = 8080;
#define MAXPOLLNUM 10
int main() {//第一步:创建监听套接字,得到监听文件描述符lfdint lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd < 0) {perror("socket");exit(-1);}//端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)int opt = 1;int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));if (isSetOk == -1) {perror("setsockopt");exit(-1);}//第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口//int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址//if (isTransOk != 1) perror("inet_pton");//sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isBindOk != 0) {perror("bind");exit(-1);}//第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。//如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8if (isListenOk != 0) {perror("listen");exit(-1);}//第四步:接收客户端连接sockaddr_in sockaddrClient;socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的// 调用epoll_create()创建一个epoll实例int epfd = epoll_create(100);//返回操作epoll实例的文件描述符if (epfd == -1) {perror("epoll_create");exit(-1);}//初始化,先把监听套接字加入进去epoll_event event;event.events = EPOLLIN;event.data.fd = lfd;// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息isSetOk = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &event);if (isSetOk == -1) {perror("epoll_ctl"); exit(-1);}epoll_event epoll_events[MAXPOLLNUM];while (1) {//nfds: 这个是第一个参数数组中最后一个有效元素的下标 + 1int changeNum = epoll_wait(epfd, epoll_events, MAXPOLLNUM, -1);//阻塞监听if (changeNum == -1) {perror("epoll_wait"); exit(-1);}else if (changeNum == 0) continue;//阻塞一定时间后没有监听到任何描述符,前面-1已经设置了永久阻塞,这一步是不会发生的else {//首先检测是否有客户端连接到来,之后再处理已经连接的客户端通信for (int i = 0; i < changeNum; i++) {int curfd = epoll_events[i].data.fd;if (curfd == lfd) {int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的客户端连接,并建立一个新的文件描述符用于和该客户端进行连接if (cfd == -1) {if (errno == EINTR) {std::cout << "EINTR";continue; }perror("accept");exit(-1);}std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;//将新的客户端连接注册进去event.events = EPOLLIN;event.data.fd = cfd;int isSetOk = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);if (isSetOk == -1) {perror("epoll_ctl"); exit(-1);}} else {//处理已连接客户端的通信char buf[1024];int readLen = read(curfd, buf, sizeof(buf));if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(curfd);epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);//从实例中删除该客户端连接的文件描述符continue;} else {perror("read");close(curfd);epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);continue;}//读到数据后进行回传const char* message = "this is server:";int writeLen = write(curfd, message,strlen(message));if (writeLen < 0) {perror("write");close(curfd);epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);continue;}writeLen = write(curfd, buf,readLen);if (writeLen < 0) {perror("write");close(curfd);epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);continue;}} }}}close(lfd);return 0;
}

连续打印客户端代码

//构建客户端
//TCP构建客户端
/*
1.socket,得到监听fd
2.connect建立连接
3.send/write
4.recv/read
7.close
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
const int PORT = 8080;
int main() {//第一步:创建套接字用户通信int cfd = socket(AF_INET, SOCK_STREAM, 0);if (cfd < 0) {perror("socket");exit(-1);}//连接服务器:需要指定连接的服务器ip和端口sockaddr_in sockaddrServer;sockaddrServer.sin_port = htons(PORT);//端口号sockaddrServer.sin_family = AF_INET;sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//以这个ip地址访问服务器,其实就是本机的虚拟网卡IPint isConnectOK = connect(cfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));if (isConnectOK == -1) {perror("connect");exit(-1);}char buf[1024];int num = 0;while (1) {num++;sprintf(buf,"this is count:%d\n",num);//fgets读完之后buf的末尾会有一个\n需要特别注意一下int writeLen = send(cfd, buf,strlen(buf),0);if (writeLen < 0) {perror("send");break;}sleep(3);int readLen = recv(cfd, buf, sizeof(buf),0);if (readLen > 0) {//打印读到的信息buf[readLen] = '\0';printf("%s",buf);} else if(readLen == 0) {//客户端已关闭连接close(cfd);break;//退出与客户端的信息交互} else {perror("recv");break;}}return 0;
}

相关文章:

  • 【JAVA】中文我该怎么排序?
  • 豪越智能仓储:为消防应急物资管理“上锁”
  • PyTorch进阶实战指南:02分布式训练深度优化
  • 使用Tkinter写一个发送kafka消息的工具
  • 如何从 iPhone 获取照片:5 个有效解决方案
  • 【鸿蒙开发】Hi3861学习笔记-DHT11温湿度传感器
  • window 显示驱动开发-设置内存分配的大小和间距
  • Redis Cluster动态扩容:架构原理与核心机制解析
  • 03-Web后端基础(Maven基础)
  • 牛客网 NC16407 题解:托米航空公司的座位安排问题
  • 《深度学习入门》第2章 感知机
  • 基于Resnet-34的树叶分类(李沐深度学习基础竞赛)
  • 【AI News | 20250521】每日AI进展
  • 【图数据库】--Neo4j 安装
  • opencv的图像卷积
  • opencv_version_win32
  • 【面经分享】微派网络一面
  • openCV1.1 Mat对象
  • Memory模块是agent的一个关键组件
  • 用java实现内网通讯,可多开客户端链接同一个服务器
  • 网站用户维度/做个网站需要多少钱
  • 资料填写网站类型怎么做/新闻10条摘抄大全
  • 佛山高端网站建设公司/百度竞价开户3000
  • 衡水企业做网站费用/发外链比较好的平台
  • python语言好学吗/seo怎么优化效果更好
  • 做男女的那个视频网站/网站制作基本流程