0. 背景
- 阻塞IO操作
通常IO操作(比如read和write)都是阻塞I/O的,当调用read时,如果没有数据收到,线程或者进程就会被挂起,直到收到数据。
阻塞IO.png
当服务器处理1000个连接,但是只有很少连接执行IO操作,那么需要1000个线程或进程来处理1000个连接,而1000个线程大部分是被挂起的。
-
线程内存和切换开销
由于CPU的核数或超线程数一般都不大,比如4,8,16,32,64,128,比如4个核要跑1000个线程,那么每个线程的时间槽非常短,而线程切换非常频繁。 -
线程是有内存开销的,1个线程可能需要512K(或2M)存放栈,那么1000个线程就要512M(或2G)内存。
-
线程的切换,或者说上下文切换是有CPU开销的,当大量时间花在上下文切换的时候,分配给真正的操作的CPU就要少很多。
1. IO多路复用
I/O多路复用:多路网络连接复用一个IO线程。
使用一个线程来检查I/O流(Socket)的就绪状态。通过记录跟踪每个I/O流(Socket)的状态,来同时管理多个I/O流 。
- MUX=multiplexing
多个Socket复用功能是在内核驱动实现的。
- IO多路复用
在处理1000个连接时,只需要1个线程监控就绪状态,就绪的连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。
I/O ready 事件的通知是以一个监听集合为单位完成的。multiplex 的是监听集合,并非 I/O 本身。
- 优点
开销低 - 缺点
编程复杂度高
2. 分类
2.1 Select模式
2.1.1 结构体
fd_set:描述符集合(long类型数组)
2.1.2 函数
监听描述符事件,如果描述符集合中没有就绪,等待;反之,函数返回,把描述符集合清空,并设置已经就绪的描述符(设置为1)。
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout)
- 参数
No. | 参数 | 含义 |
---|---|---|
1 | maxfd | 需要监视的最大的文件描述符值+1 |
2 | rdset | 需要检测的可读文件描述符的集合 |
3 | wrset | 需要检测的可写文件描述符的集合 |
4 | exset | 需要检测的异常文件描述符的集合 |
5 | timeout | 超时时间 |
- 返回值
No. | 返回值 | 含义 |
---|---|---|
1 | -1 | 出错 |
2 | =0 | 超时 |
3 | >0 | 获取到数据 |
2.1.3 宏定义
No. | 参数 | 含义 |
---|---|---|
1 | FD_ZERO(fd_set *fdset) | 清空文件描述符集 |
2 | FD_SET(int fd,fd_set *fdset) | 设置监听的描述符(把监听的描述符设置为1) |
3 | FD_CLR(int fd,fd_set *fdset) | 清除监听的描述符(把监听的描述符设置为0) |
4 | FD_ISSET(int fd,fd_set *fdset) | 判断描述符是否设置(判断描述符是否设置为1) |
5 | FD_SETSIZE | 256 |
2.1.4 就绪条件
2.1.5 编码流程
- 定义描述符集
- 清空描述符集
- 设置指定的描述符并获取最大的描述符值+1
- 等待描述符就绪
- 判断已就绪的描述符,并做对应处理。
2.1.6 代码结构
以UDP单播形式为例
- 定义描述符集合
fd_set 是一个长整型的数组
fd_set有两个作用:1. 告诉select监听哪些fd;2. 返回告诉FD哪些程序需要处理
fd_set fdset;
- 清空描述符结合
FD_ZERO(&fdset);
cout << hex << fdset.fds_bits[0] << endl;
- 设置监听的描述符
FD_SET(connfd,&fdset); // 连接描述符
FD_SET(STDIN_FILENO,&fdset); // 标准输入描述符
- 最大描述符值+1
int maxfdp1 = max(connfd,STDIN_FILENO)+1;
- 阻塞监听
if(select(maxfdp1,&fdset,NULL,NULL,NULL) > 0) {
cout << "select:" << hex << fdset.fds_bits[0] << endl;
- 判断是哪个描述符有数据
if(FD_ISSET(connfd,&fdset)) {
// 接收数据
char buffer[1024];
recvfrom(connfd,buffer,1024,0,NULL,NULL);
cout << buffer << endl;
}
if(FD_ISSET(STDIN_FILENO,&fdset)) {
// 3.发送数据
string message;
getline(cin,message);
sendto(connfd,message.c_str(),message.size()+1,0,(struct sockaddr*)&local_addr,sizeof(local_addr));
}
- 发送端完整代码
#include <iostream>
#include <thread>
#include <arpa/inet.h> // socket
#include <unistd.h>
#include <sys/select.h>
using namespace std;
// ./unicast_send IP Port
int main(int argc,char* argv[]) {
if(3!=argc) {
cout << "Usage:" << argv[0] << " IP Port" << endl;
return 1;
}
// 1.创建套接字
int connfd = socket(AF_INET,SOCK_DGRAM,0);
if(-1 == connfd) {
cout << "create socket error" << endl;
return 1;
}
cout << "connfd:" << connfd << endl;
cout << "STDIN_FILENO:" << STDIN_FILENO << endl;
// 2.绑定端口和地址
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = inet_addr(argv[1]);
local_addr.sin_port = htons(atoi(argv[2]));
// 定义描述符集合
fd_set fdset;
for(;;) { // 循环
// 清空描述符结合
FD_ZERO(&fdset);
cout << hex << fdset.fds_bits[0] << endl;
// 设置监听的描述符
FD_SET(connfd,&fdset); // 连接描述符
FD_SET(STDIN_FILENO,&fdset); // 标准输入描述符
// 1001
cout << hex << fdset.fds_bits[0] << endl;
// 最大描述符值+1
int maxfdp1 = max(connfd,STDIN_FILENO)+1;
// 阻塞监听
if(select(maxfdp1,&fdset,NULL,NULL,NULL) > 0) {
cout << "select:" << hex << fdset.fds_bits[0] << endl;
// 判断是哪个描述符有数据
if(FD_ISSET(connfd,&fdset)) {
// 接收数据
char buffer[1024];
recvfrom(connfd,buffer,1024,0,NULL,NULL);
cout << buffer << endl;
}
if(FD_ISSET(STDIN_FILENO,&fdset)) {
// 3.发送数据
string message;
getline(cin,message);
sendto(connfd,message.c_str(),message.size()+1,0,(struct sockaddr*)&local_addr,sizeof(local_addr));
}
}
}
// 4.关闭套接字
close(connfd);
}
- 接收端完整代码
#include <iostream>
#include <thread>
#include <arpa/inet.h> // socket
#include <unistd.h>
#include <strings.h>
using namespace std;
// ./unicast_recv IP Port
int main(int argc,char* argv[]) {
if(3!=argc) {
cout << "Usage:" << argv[0] << " IP Port" << endl;
return 1;
}
// 1.创建套接字
int connfd = socket(AF_INET,SOCK_DGRAM,0);
if(-1 == connfd) {
cout << "create socket error" << endl;
return 1;
}
// 2.绑定端口和地址
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = inet_addr(argv[1]);
local_addr.sin_port = htons(atoi(argv[2]));
if(-1 == bind(connfd,(struct sockaddr*)&local_addr,sizeof(local_addr))) {
cout << "bind error" << endl;
return 1;
}
struct sockaddr_in remote_addr;
bzero(&remote_addr,sizeof(remote_addr));
socklen_t len;
fd_set fdset;
while(true) {
FD_ZERO(&fdset);
FD_SET(connfd,&fdset);
FD_SET(STDIN_FILENO,&fdset);
int maxfdp1 = connfd+1; // 最大描述符值+1
if(select(maxfdp1,&fdset,NULL,NULL,NULL) > 0) {
if(FD_ISSET(connfd,&fdset)) {
// 接收数据
char buffer[1024];
recvfrom(connfd,buffer,1024,0,(struct sockaddr*)&remote_addr,&len);
cout << buffer << endl;
}
if(FD_ISSET(STDIN_FILENO,&fdset)) {
// 发送数据
string message;
getline(cin,message);
sendto(connfd,message.c_str(),message.size()+1,0,(struct sockaddr*)&remote_addr,len);
}
}
}
close(connfd);
}
需要注意的是,在测试时,需要先开启接收端,然后才能顺利完整两边的通信。
2.1.7 注意
- fd_set可容纳最大描述符数为FD_SETSIZE。
- 每一次select()前,必须重新设置描述符,如果设置了新的描述符,需要重新计算maxfdp1。
- 如果删除了描述符,需要把对应描述符的fd_set,执行FD_CLR操作
2.1.8 缺点
- 需要修改传入的参数数组
- 不能确切指定有数据的socket
- 只能监视FD_SETSIZE个链接
- 线程不安全
以TCP形式为例
- 服务端完整代码
#include <iostream>
#include <list>
#include <thread>
#include <algorithm>
#include <sys/socket.h> // socket
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
using namespace std;
void show_connect(int fd) {
struct sockaddr_in local_addr;
socklen_t local_addr_len = sizeof(local_addr);
bzero(&local_addr,local_addr_len);
getsockname(fd,(struct sockaddr*)&local_addr,&local_addr_len);
cout << "local " << inet_ntoa(local_addr.sin_addr) << ":" << ntohs(local_addr.sin_port) << endl;
struct sockaddr_in remote_addr;
socklen_t remote_addr_len = sizeof(remote_addr);
bzero(&remote_addr,remote_addr_len);
getpeername(fd,(struct sockaddr*)&remote_addr,&remote_addr_len);
cout << "remote " << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
}
// ./server ip port
int main(int argc,char* argv[]) {
cout << FD_SETSIZE << endl;
if(3!=argc) {
cout << "Usage:" << argv[0] << " IP port" << endl;
return 1;
}
// 1. 监听套接字
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == listenfd) {
cout << "listen socket error" << endl;
return 1;
}
// 设置端口重复利用
int flag = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));
// 2. 绑定
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET; // 协议
local_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
local_addr.sin_port = htons(atoi(argv[2])); // 端口号
if(-1 == bind(listenfd,(struct sockaddr*)&local_addr,sizeof(local_addr))) {
cout << "bind error" << endl;
return 1;
} else {
cout << "bind success" << endl;
}
// 3. 监听设置
if(-1==listen(listenfd,10)) {
cout << "listen error" << endl;
return 1;
} else {
cout << "listen success" << endl;
}
list<int> fds;
fd_set fdset;
while(true) {
FD_ZERO(&fdset);
FD_SET(listenfd,&fdset);
FD_SET(STDIN_FILENO,&fdset);
for(auto fd:fds) {
FD_SET(fd,&fdset);
}
int maxfdp1 = (fds.empty()?listenfd:*max_element(fds.begin(),fds.end()))+1;
if(select(maxfdp1,&fdset,NULL,NULL,NULL)>0) {
if(FD_ISSET(STDIN_FILENO,&fdset)) {
string message;
getline(cin,message);
cin.clear(); // 清空输入出错
if(!message.empty()) {
message = "广告:" + message;
for(auto fd:fds) {
write(fd,message.c_str(),message.size()+1);
}
}
}
if(FD_ISSET(listenfd,&fdset)) {
struct sockaddr_in remote_addr;
bzero(&remote_addr,sizeof(remote_addr));
socklen_t remote_addr_len = sizeof(remote_addr);
int connfd = accept(listenfd,(struct sockaddr*)&remote_addr,&remote_addr_len);
//int connfd = accept(listenfd,NULL,NULL);
if(-1 == connfd) {
cout << "accept error" << endl;
return 1;
} else {
cout << "accept success" << endl;
cout << inet_ntoa(remote_addr.sin_addr) <<":" << ntohs(remote_addr.sin_port) << endl;
show_connect(connfd);
fds.push_back(connfd);
}
}
for(auto connfd:fds) {
if(FD_ISSET(connfd,&fdset)) {
char buffer[1024] = {0};
int n = read(connfd,buffer,1024);// 读取客户端发过来的信息
if(n == 0) {
cout << "client exit" << endl;
close(connfd); // 关掉退出的连接
fds.remove(connfd);
break;
} else {
for(auto fd:fds) { // 发送信息给所有的客户端
if(fd==connfd) continue;
write(fd,buffer,1024);
}
}
}
}
}
}
close(listenfd);
}
- 客户端完整代码
#include <iostream>
#include <thread>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <strings.h>
using namespace std;
string name;
void show_connect(int fd) {
// 获取本地地址和端口
struct sockaddr_in local_addr;
socklen_t local_addr_len = sizeof(local_addr);
bzero(&local_addr,local_addr_len);
getsockname(fd,(struct sockaddr*)&local_addr,&local_addr_len);
cout << "local " << inet_ntoa(local_addr.sin_addr) << ":" << ntohs(local_addr.sin_port) << endl;
// 获取远程地址和端口
struct sockaddr_in remote_addr;
socklen_t remote_addr_len = sizeof(remote_addr);
bzero(&remote_addr,remote_addr_len);
getpeername(fd,(struct sockaddr*)&remote_addr,&remote_addr_len);
cout << "remote " << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
}
// ./clinet IP port name
int main(int argc,char* argv[]) {
if(4!=argc) {
cout << "Usage:" << argv[0] << " IP port name" << endl;
return 1;
}
name = argv[3];
// 1.创建连接套接字
int connfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == connfd) {
cout << "socket error" << endl;
return 1;
}
// 2. 连接服务器
struct sockaddr_in remote_addr;
remote_addr.sin_family = AF_INET; // 协议
remote_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
remote_addr.sin_port = htons(atoi(argv[2])); // 端口号
if(-1==connect(connfd,(struct sockaddr*)&remote_addr,sizeof(remote_addr))) {
cout << "connect error" << endl;
return 1;
} else {
cout << "connect success" << endl;
show_connect(connfd);
}
fd_set fdset;
for(;;) {
FD_ZERO(&fdset);
FD_SET(STDIN_FILENO,&fdset);
FD_SET(connfd,&fdset);
int maxfdp1=connfd+1;
if(select(maxfdp1,&fdset,NULL,NULL,NULL) > 0) {
if(FD_ISSET(STDIN_FILENO,&fdset)){
// 3.发送信息
string message;
getline(cin,message);
message = name + ":" + message;
write(connfd,message.c_str(),message.size()+1);
}
if(FD_ISSET(connfd,&fdset)){
// 4.接收数据
char buffer[1024] = {0};
int len = read(connfd,buffer,sizeof(buffer));
if(0==len) {
cout << "server exit" << endl;
break;
} else {
cout << buffer << endl;
}
}
}
}
// 5. 关闭套接字
close(connfd);
return 0;
}
- 查看文件描述符信息
2.2 Poll模式
2.2.1 背景
优点
- 不需要不修改传入的参数数组
- 可以监视任意个链接cat /proc/sys/fs/file-max
缺点
- 不能确切指定有数据的socket
- 线程不安全
2.2.2 结构体
- struct pollfd
成员 | 含义 |
---|---|
fd | 描述符 |
events | 监听事件,主要用于设置监听事件 |
revents | 实际触发的事件,用于判断触发的事件 |
2.2.3 函数
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout)
- 参数
No. | 参数 | 含义 |
---|---|---|
1 | fdarray | struct pollfd数组指针 |
2 | nfds | 数组元素个数 |
3 | timeout | 等待时间。INFTIM:永远等待;0:立即返回;>0:等待秒数; |
- struct pollfd
No. | 参数 | 含义 |
---|---|---|
1 | fd | 文件描述符 |
2 | events | 监视fd事件,监视事件可以是输入事件,可以是输出事件。 |
3 | revents | fd实际发生的事件,监视事件可以是输入事件,可以是输出事件,还可以是出错事件。 |
- fd输入事件
No. | 参数 | 含义 |
---|---|---|
1 | POLLRDNORM | 普通数据 |
2 | POLLRDBAND | 优先级带数据 |
3 | POLLIN | 普通或者优先级带数据 |
- fd输出事件
No. | 参数 | 含义 |
---|---|---|
1 | POLLWRNORM | 普通数据 |
2 | POLLWRBAND | 优先级带数据 |
3 | POLLOUT | 普通或者优先级带数据 |
- fd出错事件
No. | 参数 | 含义 |
---|---|---|
1 | POLLERR | 发生错误 |
2 | POLLHUP | 发生挂起 |
3 | POLLNVAL | 描述符非法 |
- 返回值
No. | 返回值 | 含义 |
---|---|---|
1 | 0 | 超时 |
2 | -1 | 出错 |
3 | 正数 | 就绪描述符个数 |
2.2.4 概念
数据类型
No. | 类型 | 实例 |
---|---|---|
1 | 普通数据 | 正规的TCP数据,所有的UDP数据 |
2 | 优先级带数据 | TCP带外数据 |
TCP带外数据数据中的急救车
2.2.5 编码流程
- 定义pollfd结构体数组
- 初始化pollfd结构体数组
- 设置监听poll事件
- 等待poll事件
- 判断触发事件的描述符,并做对应处理。
2.2.6 触发事件
2.2.7 代码结构
// 定义pollfd结构体数组
struct pollfd pollfds[OPEN_MAX];
// 初始化pollfd结构体数组
int i;
for(i=0;i<OPEN_MAX;i++){
pollfds[i].fd = -1;
}
int pollfds_cnt = 0;
// 设置监听事件
pollfds[0].fd = fd1;
pollfds[0].event = POLLRDNOM;
pollfds_cnt++;
pollfds[1].fd = fd1;
pollfds[1].event = POLLRDNORM;
pollfds_cnt++;
// 等待poll事件
if(poll(pollfds,pollfds_cnt,INFTIM)>0){
int i;
for(i=0;i<pollfds_cnt;i++){
// 判断触发事件的描述符
if(pollfds[i].fd == fd1 && pollfds[i].revent & POLLRDNORM){
// do something
}
if(pollfds[i].fd == fd2 && pollfds[i].revent & POLLRDNORM){
// do something
}
}
}
2.2.8 注意
struct pollfd数组的最大数是OPEN_MAX(或者linux/fs.h中的INR_OPEN_MAX )
还可以通过如下方式查看:
- cat /proc/sys/fs/file-max
- ulimit
2.2.9 示例
poll()实现服务器tcp_server_poll.cpp
#include <iostream>
#include <list>
#include <thread>
#include <algorithm>
#include <sys/poll.h>
#include <linux/fs.h> //INR_OPEN_MAX
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
using namespace std;
void show_connect(int fd) {
struct sockaddr_in local_addr;
socklen_t local_addr_len = sizeof(local_addr);
bzero(&local_addr,local_addr_len);
getsockname(fd,(struct sockaddr*)&local_addr,&local_addr_len);
cout << "local " << inet_ntoa(local_addr.sin_addr) << ":" << ntohs(local_addr.sin_port) << endl;
struct sockaddr_in remote_addr;
socklen_t remote_addr_len = sizeof(remote_addr);
bzero(&remote_addr,remote_addr_len);
getpeername(fd,(struct sockaddr*)&remote_addr,&remote_addr_len);
cout << "remote " << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
}
void Show(struct pollfd* fds,int n){
for(int i=0;i<n;++i){
cout << fds[i].fd << " ";
}
cout << endl;
}
// ./server ip port
int main(int argc,char* argv[]) {
cout << FD_SETSIZE << endl;
if(3!=argc) {
cout << "Usage:" << argv[0] << " IP port" << endl;
return 1;
}
// 1. 监听套接字
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == listenfd) {
cout << "listen socket error" << endl;
return 1;
}
// 设置端口重复利用
int flag = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));
// 2. 绑定
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET; // 协议
local_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
local_addr.sin_port = htons(atoi(argv[2])); // 端口号
if(-1 == bind(listenfd,(struct sockaddr*)&local_addr,sizeof(local_addr))) {
cout << "bind error" << endl;
return 1;
} else {
cout << "bind success" << endl;
}
// 3. 监听设置
if(-1==listen(listenfd,10)) {
cout << "listen error" << endl;
return 1;
} else {
cout << "listen success" << endl;
}
// list<int> fds;
struct pollfd fds[INR_OPEN_MAX];
for(auto& fd:fds){
fd.fd = -1;
}
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLRDNORM;
fds[1].fd = listenfd;
fds[1].events = POLLRDNORM;
int count = 2;
while(true) {
Show(fds,count);
if(poll(fds,count,-1)>0) {
if(fds[0].revents & POLLRDNORM){
string message;
getline(cin,message);
cin.clear(); // 清空输入出错
if(!message.empty()) {
message = "广告:" + message;
for(int i=2;i<count;++i) {
write(fds[i].fd,message.c_str(),message.size()+1);
}
}
}
if(fds[1].revents & POLLRDNORM){
struct sockaddr_in remote_addr;
bzero(&remote_addr,sizeof(remote_addr));
socklen_t remote_addr_len = sizeof(remote_addr);
int connfd = accept(listenfd,(struct sockaddr*)&remote_addr,&remote_addr_len);
//int connfd = accept(listenfd,NULL,NULL);
if(-1 == connfd) {
cout << "accept error" << endl;
return 1;
} else {
cout << "accept success" << endl;
cout << inet_ntoa(remote_addr.sin_addr) <<":" << ntohs(remote_addr.sin_port) << endl;
show_connect(connfd);
// fds.push_back(connfd);
fds[count].fd = connfd;
fds[count].events = POLLRDNORM;
fds[count].revents = 0;// 清零
++count;
}
}
for(int i=2;i<count;++i){
if(fds[i].revents & POLLRDNORM){
int connfd = fds[i].fd;
char buffer[1024] = {0};
int n = read(connfd,buffer,1024);// 读取客户端发过来的信息
if(n == 0) {
cout << "client exit" << endl;
close(connfd); // 关掉退出的连接
// 把关闭的fd从数组中移除
for(int j=i;j<count-1;++j){
fds[j] = fds[j+1];
}
--count;
break;
} else {
for(int j=2;j<count;++j) { // 发送信息给所有的客户端
if(i==j) continue;
write(fds[j].fd,buffer,1024);
}
}
}
}
}
}
close(listenfd);
}
poll()实现客户端tcp_client_poll.cpp
#include <iostream>
#include <thread>
#include <arpa/inet.h>
#include <unistd.h>
//#include <sys/select.h>
#include <sys/poll.h> // poll()
#include <strings.h>
#include <linux/fs.h> // OPEN_MAX
using namespace std;
string name;
void show_connect(int fd) {
// 获取本地地址和端口
struct sockaddr_in local_addr;
socklen_t local_addr_len = sizeof(local_addr);
bzero(&local_addr,local_addr_len);
getsockname(fd,(struct sockaddr*)&local_addr,&local_addr_len);
cout << "local " << inet_ntoa(local_addr.sin_addr) << ":" << ntohs(local_addr.sin_port) << endl;
// 获取远程地址和端口
struct sockaddr_in remote_addr;
socklen_t remote_addr_len = sizeof(remote_addr);
bzero(&remote_addr,remote_addr_len);
getpeername(fd,(struct sockaddr*)&remote_addr,&remote_addr_len);
cout << "remote " << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
}
// ./clinet IP port name
int main(int argc,char* argv[]) {
if(4!=argc) {
cout << "Usage:" << argv[0] << " IP port name" << endl;
return 1;
}
name = argv[3];
// 1.创建连接套接字
int connfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == connfd) {
cout << "socket error" << endl;
return 1;
}
// 2. 连接服务器
struct sockaddr_in remote_addr;
remote_addr.sin_family = AF_INET; // 协议
remote_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
remote_addr.sin_port = htons(atoi(argv[2])); // 端口号
if(-1==connect(connfd,(struct sockaddr*)&remote_addr,sizeof(remote_addr))) {
cout << "connect error" << endl;
return 1;
} else {
cout << "connect success" << endl;
show_connect(connfd);
}
//fd_set fdset;
// 定义pollfd结构体数组,最大为OPEN_MAX个
// fd 要监听的fd
// events 要监听的事件
// revents 发生的事件
struct pollfd fds[INR_OPEN_MAX];
// 数组中的fd全部设置成-1
for(int i=0;i<INR_OPEN_MAX;++i){
fds[i].fd = -1; // -1表示不监听
}
int count = 0;//当前监听fd的个数
// 设置监听
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLRDNORM;
fds[1].fd = connfd;
fds[1].events = POLLRDNORM;
count = 2;
for(;;) {
if(poll(fds,count,-1)>0) {
if(fds[0].revents & POLLRDNORM){
// 3.发送信息
string message;
getline(cin,message);
message = name + ":" + message;
write(connfd,message.c_str(),message.size()+1);
}
if(fds[1].revents & POLLRDNORM){
// 4.接收数据
char buffer[1024] = {0};
int len = read(connfd,buffer,sizeof(buffer));
if(0==len) {
cout << "server exit" << endl;
break;
} else {
cout << buffer << endl;
}
}
/*
if(FD_ISSET(STDIN_FILENO,&fdset)){
// 3.发送信息
string message;
getline(cin,message);
message = name + ":" + message;
write(connfd,message.c_str(),message.size()+1);
}
if(FD_ISSET(connfd,&fdset)){
// 4.接收数据
char buffer[1024] = {0};
int len = read(connfd,buffer,sizeof(buffer));
if(0==len) {
cout << "server exit" << endl;
break;
} else {
cout << buffer << endl;
}
}*/
}
}
// 5. 关闭套接字
close(connfd);
return 0;
}
2.3 Epool模式
2.3.1 背景
优点
- 能确切指定有数据的socket
- 线程安全
2.3.2 结构体
struct epoll_event
成员 | 含义 |
---|---|
data.fd | 描述符 |
events | 设置/获取epoll事件 |
结构体epoll_event被用于注册所感兴趣的事件和回传所发生待处理的事件,定义如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;//保存触发事件的某个文件描述符相关的数据
struct epoll_event {
__uint32_t events; /* epoll event */
epoll_data_t data; /* User data variable */
};
其中events表示感兴趣的事件和被触发的事件,可能的取值为:
No. | 事件 | 含义 |
---|---|---|
1 | EPOLLIN | 表示对应的文件描述符可以读 |
2 | EPOLLOUT | 表示对应的文件描述符可以写 |
3 | EPOLLPRI | 表示对应的文件描述符有紧急的数可读 |
4 | EPOLLERR | 表示对应的文件描述符发生错误 |
5 | EPOLLHUP | 表示对应的文件描述符被挂断 |
6 | EPOLLET | ET的epoll工作模 |
2.3.2 函数
No. | 操作 | 函数 |
---|---|---|
1 | 创建 | int epoll_create(int size) |
2 | 控制 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) |
3 | 轮询I/O事件 | int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout) |
①创建
int epoll_create(int size)
- 参数
No. | 参数 | 含义 |
---|---|---|
1 | size | 监听的数目 |
- 返回值
文件描述符
可以在/proc/PID/fd/查看
② 控制
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
- 参数
No. | 参数 | 含义 |
---|---|---|
1 | epfd | epoll文件描述符 |
2 | op | 操作 |
3 | fd | 关联的文件描述符 |
4 | event | 指向epoll_event的指针 |
- 操作
No. | 参数 | 含义 |
---|---|---|
1 | EPOLL_CTL_ADD | 注册 |
2 | EPOLL_CTL_MOD | 修改 |
3 | EPOLL_CTL_DEL | 删除 |
- 返回值
No. | 返回值 | 含义 |
---|---|---|
1 | 0 | 成功 |
2 | -1 | 失败 |
③ 轮询I/O事件
int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout)
- 参数
No. | 参数 | 含义 |
---|---|---|
1 | epfd | epoll文件描述符 |
2 | epoll_event | 用于回传代处理事件的数组 |
3 | maxevents | 每次能处理的事件数 |
4 | timeout | 等待I/O事件发生的超时值ms,-1:永不超时;0: 立即返回 |
- 返回值
No. | 返回值 | 含义 |
---|---|---|
1 | 正数 | 发生事件数 |
2 | -1 | 错误 |
2.3.3 编码流程
- 创建epoll描述符
- 注册epoll事件
- 等待epoll事件
- 判断触发epoll事件的描述符和事件
- 关闭epoll描述符
2.3.4 触发条件
- ET(Edge Triggered)模式–边沿触发
No. | 操作 | 触发条件 |
---|---|---|
1 | 读 | 接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件 |
2 | 写 | 发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件 |
- LT(Level Triggered)模式–水平触发
No. | 操作 | 触发条件 |
---|---|---|
1 | 读 | 接收缓冲区不为空,有数据可读,读事件一直触发 |
2 | 写 | 发送缓冲区不满,可以继续写入数据,写事件一直触发 |
2.3.5 代码结构
LT代码结构
// 创建epoll描述符
int epollfd = epoll_create(OPEN_MAX);
// 注册epoll事件
struct epoll_event evt;
evt.data.fd = fd1;
evt.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd1,&evt);
evt.data.fd = fd2;
evt.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd2,&evt);
// 等待epoll事件
int evts_cnt = 2;
struct epoll_event evts[evts_cnt];
int fd_cnt = epoll_wait(epollfd,&evts,evts_cnt,-1);:
int i;
for(i=0;i<fd_cnt,i++){
// 判断触发epoll事件的描述符和事件
if(evts[i].data.fd == fd1 && evts[i].events & EPOLLIN){
// do something
}
if(evts[i].data.fd == fd2 && evts[i].events & EPOLLIN){
// do something
}
}
// 关闭epoll描述符
close(epollfd);
项目描述:运用 socket 编程,实现了服务端对客户端的广播和转发客户端信息实现客户端聊天操作,并使用 i/o 复用技术中的 epoll 模式解决了一个线程可以处理大量用户连接服务器的请求,提高了服务器并发连接的数量。同时利用线程池来管理各个线程的工作,避免了创建和销毁线程的开销,因此使服务器更加高效。
epoll()实现服务器tcp_server_epoll.cpp
// 一服务器 -> 多客户端
// 三次握手主要是创建连接
// 四次挥手主要是释放资源
// I/O复用 epoll模式
#include <iostream>
#include <list> // remove()
#include <thread>
#include <algorithm> // max_element()
#include <sys/epoll.h> // epoll()
#include <linux/fs.h> //INR_OPEN_MAX
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h> // bzero()
using namespace std;
void show_connect(int fd) {
struct sockaddr_in local_addr; // 服务器网络地址结构体
socklen_t local_addr_len = sizeof(local_addr);
bzero(&local_addr,local_addr_len); //数据初始化--清零
getsockname(fd,(struct sockaddr*)&local_addr,&local_addr_len);
cout << "local " << inet_ntoa(local_addr.sin_addr) << ":" << ntohs(local_addr.sin_port) << endl;
// 网络字节序长整型转点分十进制数串, 把unsigned short类型从网络序转换到主机序 IPV4专用
struct sockaddr_in remote_addr; // IPv4套接字地址结构 客户端网络地址结构体
socklen_t remote_addr_len = sizeof(remote_addr);
bzero(&remote_addr,remote_addr_len); //数据初始化--清零
getpeername(fd,(struct sockaddr*)&remote_addr,&remote_addr_len);
cout << "remote " << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
}
// ./server ip port
int main(int argc,char* argv[]) {
cout << FD_SETSIZE << endl;
if(3!=argc) {
cout << "Usage:" << argv[0] << " IP port" << endl;
return 1;
}
// 1. 监听套接字
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == listenfd) {
cout << "listen socket error" << endl;
return 1;
}
// 设置端口重复利用
// 为了避免端口被占用,想要再次使用同一个端口
// 设置端口重复利用(一般用在调试中)
int flag = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));
// 基本套接口,让端口释放后立即就可以被再次使用(TIME_WAIT状态下)
// 2. 绑定
struct sockaddr_in local_addr; // 服务器网络地址结构体
local_addr.sin_family = AF_INET; // 协议
local_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
local_addr.sin_port = htons(atoi(argv[2])); // 端口号 小端转大端
if(-1 == bind(listenfd,(struct sockaddr*)&local_addr,sizeof(local_addr))) {
cout << "bind error" << endl;
return 1;
} else {
cout << "bind success" << endl;
}
// 3. 监听设置
if(-1==listen(listenfd,10)) {
cout << "listen error" << endl;
return 1;
} else {
cout << "listen success" << endl;
}
// 4. 创建epoll描述符
int epollfd = epoll_create(INR_OPEN_MAX);
struct epoll_event evt;
evt.data.fd = STDIN_FILENO; // 表示标准输入
evt.events = EPOLLIN; // 表示对应的文件描述符可以读
epoll_ctl(epollfd,EPOLL_CTL_ADD,STDIN_FILENO,&evt); //注册
evt.data.fd = listenfd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&evt);
int count = 2;
list<int> fds; // 建立一个链表
while(true) {
struct epoll_event revt[count];
int revt_cnt = epoll_wait(epollfd,revt,count,-1); // 轮询I/O事件
// epoll文件描述符、用于回传代处理事件的数组、每次能处理的事件数、等待I/O事件发生的超时值ms,-1:永不超时;0: 立即返回。
for(int i=0; i<revt_cnt; ++i) {
if(revt[i].data.fd == STDIN_FILENO && revt[i].events & EPOLLIN) {
// 服务器广告线程
string message;
getline(cin,message);
cin.clear(); // 清空输入出错
if(!message.empty()) { // 判断终端输入是否为空
message = "广告:" + message;
for(auto fd:fds) {
write(fd,message.c_str(),message.size()+1);
}
}
} else if(revt[i].data.fd == listenfd && revt[i].events & EPOLLIN) {
struct sockaddr_in remote_addr;
bzero(&remote_addr,sizeof(remote_addr)); // 清空
socklen_t remote_addr_len = sizeof(remote_addr);
int connfd = accept(listenfd,(struct sockaddr*)&remote_addr,&remote_addr_len);
//int connfd = accept(listenfd,NULL,NULL);
if(-1 == connfd) {
cout << "accept error" << endl;
return 1;
} else {
cout << "accept success" << endl;
cout << inet_ntoa(remote_addr.sin_addr) <<":" << ntohs(remote_addr.sin_port) << endl;
show_connect(connfd);
struct epoll_event evt;
evt.data.fd = connfd;
evt.events = EPOLLIN; // 表示对应的文件描述符可以读
epoll_ctl(epollfd,EPOLL_CTL_ADD,connfd,&evt);
// epoll文件描述符,操作(注册),关联的文件描述符,指向epoll_event的指针
fds.push_back(connfd);
++count;
}
} else {
int connfd = revt[i].data.fd;
char buffer[1024] = {0};
int n = read(connfd,buffer,1024);// 读取客户端发过来的信息
if(n == 0) {
cout << "client exit" << endl;
close(connfd); // 关掉退出的连接
epoll_ctl(epollfd,EPOLL_CTL_DEL,connfd,revt+i);
// epoll文件描述符,操作(删除),关联的文件描述符,指向epoll_event的指针
fds.remove(connfd);
--count;
break;
} else {
for(auto fd:fds) { // 发送信息给所有的客户端
if(fd==connfd) continue;
write(fd,buffer,1024);
}
}
}
}
}
close(epollfd);// 关闭epoll描述符
// 5. 关闭套接字
close(listenfd);
}
epoll()实现客户端tcp_client_epoll.cpp
#include <iostream>
#include <thread>
#include <arpa/inet.h>
#include <unistd.h>
//#include <sys/select.h>
#include <sys/epoll.h> // epoll
#include <strings.h>
#include <linux/fs.h> // OPEN_MAX
using namespace std;
string name;
void show_connect(int fd) {
// 获取本地地址和端口
struct sockaddr_in local_addr;
socklen_t local_addr_len = sizeof(local_addr);
bzero(&local_addr,local_addr_len); // 清空 防止后面误用
getsockname(fd,(struct sockaddr*)&local_addr,&local_addr_len);
cout << "local " << inet_ntoa(local_addr.sin_addr) << ":" << ntohs(local_addr.sin_port) << endl;
// 获取远程地址和端口
struct sockaddr_in remote_addr;
socklen_t remote_addr_len = sizeof(remote_addr);
bzero(&remote_addr,remote_addr_len);
getpeername(fd,(struct sockaddr*)&remote_addr,&remote_addr_len);
cout << "remote " << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
}
// ./clinet IP port name
int main(int argc,char* argv[]) {
if(4!=argc) {
cout << "Usage:" << argv[0] << " IP port name" << endl;
return 1;
}
name = argv[3];
// 1.创建连接套接字
int connfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == connfd) {
cout << "socket error" << endl;
return 1;
}
// 2. 连接服务器
struct sockaddr_in remote_addr; // in是internet简写
remote_addr.sin_family = AF_INET; // 协议 sin是sockaddr_in的缩写
remote_addr.sin_addr.s_addr = inet_addr(argv[1]); // IP地址
remote_addr.sin_port = htons(atoi(argv[2])); // 端口号 小端转大端
if(-1==connect(connfd,(struct sockaddr*)&remote_addr,sizeof(remote_addr))) {
cout << "connect error" << endl;
return 1;
} else {
cout << "connect success" << endl;
show_connect(connfd);
}
//fd_set fdset;
// 定义pollfd结构体数组,最大为OPEN_MAX个
// fd 要监听的fd
// events 要监听的事件
// revents 发生的事件
// struct pollfd fds[INR_OPEN_MAX];
// 创建epoll描述符
int epollfd = epoll_create(2);
// 注册事件
struct epoll_event evt;
evt.data.fd = STDIN_FILENO;
evt.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,STDIN_FILENO,&evt);
evt.data.fd = connfd;
evt.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,connfd,&evt);
bool stop = false;
while(!stop) {
int count = 2;
struct epoll_event revt[2];
int revt_count = epoll_wait(epollfd,revt,count,-1);
for(int i=0; i<revt_count; ++i) {
if(revt[i].data.fd == STDIN_FILENO && revt[i].events & EPOLLIN) {
// 3.发送信息
string message;
getline(cin,message);
message = name + ":" + message;
write(connfd,message.c_str(),message.size()+1);
} else if(revt[i].data.fd == connfd && revt[i].events & EPOLLIN) {
// 4.接收数据
char buffer[1024] = {0};
int len = read(connfd,buffer,sizeof(buffer));
if(0==len) {
cout << "server exit" << endl;
epoll_ctl(epollfd,EPOLL_CTL_DEL,connfd,revt+i);
--count;
stop = true;
break;
} else {
cout << buffer << endl;
}
}
}
}
close(epollfd); // 关闭epoll描述符
// 5. 关闭套接字
close(connfd);
return 0;
}
3. 比较
3.1 select IO多路复用
- 缺点
- 只能监视FD_SETSIZE个连接
- 不能确切指定有数据的socket
- 每次需要修改传入的fd_set
- 线程不安全
3.2 poll IO多路复用
-
优点
- 不需要不修改传入的pollfd数组
- 可以监视任意个连接
-
缺点
- 不能确切指定有数据的socket
- 线程不安全
3.3 epoll IO多路复用
- 优点
- 能确切指定有数据的socket
- 线程安全
4 TCP与UDP在socket编程中的区别
1. TCP与UDP的区别 基于连接与无连接
对系统资源的要求(TCP较多,UDP少) UDP程序结构较简单 流模式与数据报模式
TCP保证数据正确性,UDP可能丢包 TCP保证数据顺序,UDP不保证
部分满足以下几点要求时,应该采用UDP 面向数据报方式 网络数据大多为短消息 拥有大量Client
对数据安全性无特殊要求
网络负担非常重,但对响应速度要求高 具体编程时的区别 socket()的参数不同
UDP Server不需要调用listen和accept UDP收发数据用sendto/recvfrom函数
TCP:地址信息在connect/accept时确定
UDP:在sendto/recvfrom函数中每次均 需指定地址信息 UDP:shutdown函数无效
2. man----socket
通过查看socket的man手册可以看到socket函数的第一个参数的值可以为下面这些值:
Name Purpose
PF_UNIX, PF_LOCAL Local communication PF_INET IPv4 Internet protocols PF_INET6 IPv6 Internet protocols PF_IPX IPX - Novell protocols
PF_NETLINK Kernel user interface device PF_X25 ITU-T X.25 / ISO-8208 protocol PF_AX25 Amateur radio AX.25 protocol
PF_ATMPVC Access to raw ATM PVCs PF_APPLETALK Appletalk
PF_PACKET Low level packet interface
3. 编程区别
通常我们在说到网络编程时默认是指TCP编程,即用前面提到的socket函数创建一个socket用于TCP通讯,函数参数我们通常填为SOCK_STREAM。即socket(PF_INET, SOCK_STREAM, 0),这表示建立一个socket用于流式网络通讯。
SOCK_STREAM这种的特点是面向连接的,即每次收发数据之前必须通过connect建立连接,也是双向的,即任何一方都可以收发数据,协议本身提供了一些保障机制保证它是可靠的、有序的,即每个包按照发送的顺序到达接收方。
而SOCK_DGRAM这种是User Datagram Protocol协议的网络通讯,它是无连接的,不可靠的,因为通讯双方发送数据后不知道对方是否已经收到数据,是否正常收到数据。任何一方建立一个socket以后就可以用sendto发送数据,也可以用recvfrom接收数据。根本不关心对方是否存在,是否发送了数据。它的特点是通讯速度比较快。大家都知道TCP是要经过三次握手的,而UDP没有。
1、 (SOCK-STREAM)流式套接字:提供一种可靠的、面向连接的双向数据传输服务,实现了数据无差错、无重复的发送。发送大批量的数据或者对数据传输有较高的要求时,可以使用流式套接字。
2、 (SOCK-DGRAM)数据报套接字: 提供一种无连接、不可靠的双向数据传输服务。数据包以独立的形式被发送,数据在传输过程中可能会丢失或重复,并且不能保证在接收端按发送顺序接收数据。出现差错的可能性较小或允许部分传输出错的应用场合,可以使用数据报套接字。
3、(SOCK-RAW)原始套接字:该套接字允许对较低层协议(如IP或ICMP)进行直接访问,常用于网络协议分析,检验新的网络协议实现,也可用于测试新配置或安装的网络设备。
基于上述不同,UDP和TCP编程步骤也有些不同,如下:
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;
TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、循环接收数据,用函数recvfrom();
5、关闭网络连接;
UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置对方的IP地址和端口等属性;
5、发送数据,用函数sendto();
6、关闭网络连接;