单进程处理单个客户端
额外知识补充:
端口号:端口号用来识别同一台计算机中进行通信的不同应用程序。
端口号 是一个 16 位整数,范围从 0 到 65535。
常见端口号: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() {
//第一步:创建监听套接字,得到监听文件描述符lfd
int 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);//表示等待连接队列的最大长度为8
if (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地址访问服务器,其实就是本机的虚拟网卡IP
int 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子进程结束时,父进程会收到这个信号
//第一步:创建监听套接字,得到监听文件描述符lfd
int 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);//表示等待连接队列的最大长度为8
if (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() {
//第一步:创建监听套接字,得到监听文件描述符lfd
int 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);//表示等待连接队列的最大长度为8
if (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]));//全部置0
sockInfos[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() {
//第一步:创建监听套接字,得到监听文件描述符lfd
int 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);//表示等待连接队列的最大长度为8
if (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);//全部置0
FD_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() {
//第一步:创建监听套接字,得到监听文件描述符lfd
int 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);//表示等待连接队列的最大长度为8
if (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: 这个是第一个参数数组中最后一个有效元素的下标 + 1
int 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() {
//第一步:创建监听套接字,得到监听文件描述符lfd
int 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);//表示等待连接队列的最大长度为8
if (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: 这个是第一个参数数组中最后一个有效元素的下标 + 1
int 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地址访问服务器,其实就是本机的虚拟网卡IP
int 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;
}