c++在windows和Linux上的网络开发流程实际上都差不多,用的API也一样
sock,bind,listen,connect,accept,aend,recv,select,gethostbyname,close
1.TCP网络通信的流程
服务端流程:
(1).创建套接字
(2).绑定IP和端口
(3).开启监听
(4).接受连接
(5).基于接受而新产生的套接字来接受和发送信息
(6).关闭套接字
客服端
(1).创建套接字
(2).绑定服务器
(3).发送信息,接受信息
(4).close
有个比较有趣的问题,服务器绑定的地址,如果只允许本地局域网连接,设置为127.0.0.1,如果允许外网连接,就设置为INADDR_ANY。客户端的端口和服务器的端口需要根据实际情况选择,1024以下的端口一般是预留给各种特殊程序的,所以最好不要占用。
服务器的端口一定要选择一个确定值,因为客户端要根据具体的地址连接到服务器。
至于客户端,它到底能不能绑定一个端口呢?
回答是可以,但是又不可以。
这是因为客户端也可以使用确定的端口地址来与服务器通信,如果你不去绑定,那么操作系统就随机给你分配一个。如果你绑定了,但是端口又是不合法的地址,那么操作系统又会给随机分配一个。就是这么一回事。
一个简单的通信例子
cpp
/*服务端*/
#include<iostream>
#include<string.h>
#include<vector>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/time.h>
#include<vector>
using namespace std;
int main()
{
//create socket
size_t socket_int = socket(AF_INET, SOCK_STREAM, 0);
if(socket_int < 0)
{
cout << "套接字创建失败" << endl;
return -1;
}
//服务端的地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(3000);
//绑定
if(bind(socket_int, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) < 0)
{
cout << "bind error" << endl;
return -1;
}
//监听
if(listen(socket_int,50) < 0)
{
cerr << "监听失败" << endl;
return -1;
}
while(true)
{
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//接受客户端连接
int clientfd = accept(socket_int, (struct sockaddr*)&clientaddr, &clientaddrlen);
if(clientfd < 0)
continue;
char recvBuf[255];
memset(recvBuf, '\0', 255);
int ret = recv(clientfd, recvBuf, 255, 0);
if(ret > 0)
{
cout << "接收到信息:" << recvBuf << endl;
ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
}
else
cout << "recv error"<<endl;
close(clientfd);
}
close(socket_int);
return 0;
}
cpp
#include<iostream>
#include<string.h>
#include<vector>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
using namespace std;
int main()
{
//create socket
size_t socket_int = socket(AF_INET, SOCK_STREAM, 0);
if(socket_int < 0)
{
cout << "套接字创建失败" << endl;
return -1;
}
//服务端的地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(3000);
//绑定
if(connect(socket_int, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) < 0)
{
cout << "bind error" << endl;
return -1;
}
char sendMessage[] = "hello world";
//发送信息
int ret = send(socket_int, sendMessage, strlen(sendMessage), 0);
if(ret != strlen(sendMessage))
{
cerr << "发送失败" << endl;
return -1;
}
char recvBuf[255];
memset(recvBuf, '\0', 255);
ret = recv(socket_int, recvBuf, 255, 0);
if(ret > 0)
{
cout << "接收到信息:" << recvBuf << endl;
}
else
cout << "recv error"<<endl;
close(socket_int);
return 0;
}
2.select函数
(1)select事件就绪
select函数是用来检测一组socket中是否有事件就绪,事件一般分为三类
(1).读事件
简单来讲就是内核缓冲区中有字符写入,或者描述符读取被关闭
(2).写事件
内核缓冲区有可以发送的字符,或者描述符的写端关闭
(3).异常事件
描述符出问题了
(2).函数描述
select(int nfds, fd_set* readfds. fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
(1).nfds:最大的事件检测值+1
(2).readfds:需要监听的写事件的fd集合
(3).writefds:需要监听的读事件的fd集合
(4).exceptfds:需要监听的异常事fd集合
timeout是一个结构体,它是一个时间设置,代表select在它规定的时间里面监听,超出这个时间就要返回。
struct timeval{
int tv_sec;//秒
int tv_usec;//微秒
};
关于这个函数的配套函数其实都是位操作。其中的核心fd_set是一个结构体
typedef struct{
long int __fds_bits[16];
}fd_set;
long int 为8个字节,因此它可以监听1024个信息
配套的函数都是宏定义实现的,我们先看看有哪些函数
(1).清空函数-把全部的位清空
void FD_ZERO(fd_set *set);
(2).删除一个位:把对应的fd从监听中取消
void FD_CLR(int fd, fd_set* set);
(3).添加一个位:添加一个fd到监听的队列中
void FD_SET(int fd, fd_set* set);
(4).判断某个fd是否有我们关注的事件
void FD_ISSET(int fd, fd_set* set);
下面我们介绍一下以下用法;
cpp
#include<iostream>
#include<string.h>
#include<vector>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/time.h>
#include<vector>
using namespace std;
int main()
{
//create socket
size_t socket_int = socket(AF_INET, SOCK_STREAM, 0);
if(socket_int < 0)
{
cout << "套接字创建失败" << endl;
return -1;
}
//服务端的地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(3000);
//绑定
if(bind(socket_int, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) < 0)
{
cout << "bind error" << endl;
return -1;
}
//监听
if(listen(socket_int,50) < 0)
{
cerr << "监听失败" << endl;
return -1;
}
vector<size_t> clientfds;
int maxfd;
while(true){
fd_set readset;
FD_ZERO(&readset);
//监听可读事件
FD_SET(socket_int, &readset);
//将客户端的事件加入到监听中
maxfd = socket_int;
int clientfdslength = clientfds.size();
for(int i = 0; i < clientfdslength; ++i)
{
if(clientfds[i] != -1)
{
FD_SET(clientfds[i], &readset);
if(maxfd < clientfds[i])
maxfd = clientfds[i];
}
}
struct timeval tm;
tm.tv_sec = 1;
tm.tv_usec = 0;
//暂时只检测可读的事件
int ret = select(maxfd+1, &readset, NULL, NULL, &tm);
if(ret < 0)
{
cout << "select error\n";
break;
}
else if(ret == 0)//检测超时
{
continue;
}
else
{
//检测事件
if(FD_ISSET(socket_int,&readset))
{
struct sockaddr_in clientaddr;
socklen_t clientlength = sizeof(struct sockaddr_in);
int clientfd = accept(socket_int, (struct sockaddr*)&clientaddr, &clientlength);
if(clientfd == -1)
{
break;
}
cout << "接受到连接" << endl;
clientfds.push_back(clientfd);
}
else
{
char recvBuf[255] = "";
size_t clientlength = clientfds.size();
for(int i = 0; i < clientlength; ++i)
{
if(clientfds[i]!=-1 && FD_ISSET(clientfds[i], &readset))
{
int length = recv(clientfds[i], recvBuf, 255, 0);
if(length <= 0)
{
cout << "数据接受失败"<<endl;
close(clientfds[i]);
clientfds[i] = -1;
continue;
}
cout << "接受到的数据:" << recvBuf << endl;
}
}
}
}
}
int clientfdslength = clientfds.size();
for(int i = 0; i < clientfdslength; ++i)
{
if(clientfds[i] != -1)
close(clientfds[i]);
}
close(socket_int);
return 0;
}
首先,我们创建一个套接字,然后绑定服务器的IP和端口,然后创建fd_set,把创建的套接字纳入到监听中,然后不断循环测试,如果检测到被修改的套接字,则给它建立一个连接,生成一个套接字,然后加入到监听组中,通过循环不断遍历,查找已经产生变化的套接字,然后接受他们的信息。
需要注意的是select在调用前后可能会修改监听的套接字,需要调用FD_ZERO和FD_SET来及时设置他们。其次time的值设定问题,如果设置这个参数的事件为0,则select立刻返回,如果设置为NULL,则会阻塞。select的参数必须是最大套接字+1
3.select的缺点
(1)每次调用select的时候,都需要把fd集合从用户态复制到内核态,这个开销在fd较多的时候开销很大,同时每次调用select都需要在内核中遍历传递进来的所有fd,这个开销在fd较多时也很大。
(2).能够监控的文件描述符也有限,一般是1024,也有512的,也有2048的
(3).select调用的时候都需要对传入的参数进行重新设定,很麻烦
4.socket的阻塞模式和非阻塞模式
(1).阻塞与非阻塞的区别
当满足条件的时候,执行线程会继续进行,但是条件不满足的时候,线程是否会继续执行下去?
如果会,则它是非阻塞。
如果不会,则它不是阻塞。
默认情况下,socket是阻塞的。
(2).设置非阻塞的方法
如果我们想要创建一个非阻塞的套接字,可以使用以下几种办法
(1).在创建套接字的时候设置type为SOCK_NONBLOCK
int sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
(2).使用accept4的第四个参数为SOCK_NONBLOCK
(3).使用万能的修改描述符函数fcntl或者ioctl函数
(3).send和recv函数在阻塞和非阻塞的表现
send函数是先把需要发送的字节从用户态复制到内核缓冲区,在TCP协议中,数据需要累积到一定的程度才会被发送出去,同样的recv会从内核缓冲区不断复制字节到用户区。这就要求服务方和用户方一致的表现。
假设出现了一种情况,接收方没有读取内核缓冲区数据,那么发送方的内核缓冲区满了怎么办?
这要分情况讨论
(1).非阻塞模式:发送方继续调用send,然后返回一个错误记录,对于recv也一样
(2).阻塞模式:阻塞在send处,对于recv也一样。
这里顺便提一下三次握手和四次挥手
(1).客户端连接服务器,发送一个seq = a
(2).服务器接收到a, 返回一个ack=a+1, 同时发送一个seq = b
(3).客户端接收到b,给服务器发送ack = b +1
如果一次连接,则客服端则无法确定与服务器是否建立了连接
如果只有两次,服务器无法确定客服端的状态
三次就刚刚好,可以相互确认
四次挥手还是以客服端为主动方
(1).客户端发送一个FIN,表示不在发送数据,但是它还可以接收数据,因为你不确定服务器还发送数据不?
(2).服务器受到到FIN,发送一个ACK,表示知道了
(3).服务器再发送一个FIN,表示服务器也没有需要发送的数据了
(4).客服端接收到之后,就知道自己也不会接收到服务器的数据了,于是就返回一个确认ACK,这是表示我知道你不会发送了,而我也不在接收,然后等待一会就自动关闭。
(3)recv和send的返回值问题
一般来讲,当返回值大于0的时候,代表成功发送的字节
但是大于0不代表正确,这是因为有可能你想要发送(接收)20个字节,但是却只发送(接收)了10个,所有你需要重新传输。
其次返回值为0的问题:
返回值为0代表对方端口关闭了,或者你发送了0个字节
最后返回值为负数:
如果是阻塞模式,它肯定错了;但是如果是非阻塞模式,这也不代表一定错了,可能是缓冲区满了无法send或者缓冲区空了无法recv,也有可能是TCP的窗口拥塞。
5.connect的阻塞和非阻塞模式
在阻塞模式下,connect函数会等待到有了明确的连接结构才会返回,否则会阻塞。
在非阻塞模式下,调用函数之后,无论成功与否,都会立刻返回,如果失败了则会返回具有明确意义的错误码,并不代表一定出错。如果我们需要在非阻塞模式下编写代码,则可以先创建阻塞模式的套接字,然后使用fcntl设置为非阻塞。如果需要再非阻塞状态下判断连接情况,就需要使用select或者poll来判断当前套接字是否可写。
6.poll函数的用法
poll函数也是用来检测一组描述符可读可写和异常事件的,
(1).函数解释
int poll(struct pollfd* fds, nfds_t nfds,int timeout);
fds是一个结构体指针,指向数组第一位。
nfds,类型其实是long int,代表数组长度
timeout代表检测时间
结构体的定义如下:
struct pollfd{
int fd;//事件集合
short event;//关心的事件组合
short revent;//检测之后得到的事件类型
};
event的事件类型如下
|------------|--------------------|----------|----------|
| 事件 | 事件描述 | 是否可以作为输入 | 是否可以作为输出 |
| POLLIN | 数据可读 | 是 | 是 |
| POLLOUT | 数据可写 | 是 | 是 |
| POLLRDNORM | 数据可读 | 是 | 是 |
| POLLRDBAND | 优先级带数据可读 | 是 | 是 |
| POLLPRI | 高级优先级数据可读 | 是 | 是 |
| POLLWRNORM | 数据可写 | 是 | 是 |
| POLLWRBAND | 优先带数据可写 | 是 | 是 |
| POLLRDHUP | TCP连接对端关闭,或者关闭了写操作 | 是 | 是 |
| POLLHUP | 挂起 | 否 | 否 |
| POLLERR | 错误 | 否 | 否 |
| POLLVAL | 文件描述符没有打开 | 否 | 否 |
poll比起select有许多优点,第一就是不用担心检测事件的数量,第二是不用重复设置监听,第三是不用买担心监听最大文件描述符最大值+1,第四是速度更快。
cpp
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<poll.h>
#include<iostream>
#include<string.h>
#include<errno.h>
#include<vector>
int main()
{
//创建套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1)
{
std::cerr << "create socket error " << std::endl;
return -1;
}
//套接字修改为非阻塞
int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if(fcntl(listenfd, F_SETFL, newSocketFlag)==-1)
{
close(listenfd);
std::cerr<<"scoket flag change error" <<std::endl;
}
//复用IP和端口
int no = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*) &no, sizeof(no));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*) &no, sizeof(no));
//设置服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(3000);
//绑定
if(bind(listenfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) == -1)
{
std::cout << "绑定失败" << std::endl;
close(listenfd);
return -1;
}
//开启监听
if(listen(listenfd, 100) == -1)
{
close(listenfd);
return -1;
}
//开始准备监听套接字和事件
std::vector<pollfd> fds;
pollfd listen_fd_info;
listen_fd_info.fd = listenfd;//先监听服务器的套接字
listen_fd_info.events = POLLIN;//监听可读事件
listen_fd_info.revents = 0;
fds.push_back(listen_fd_info);//放入监听数组
bool exist_invalid_fd;
int n;
while(true)
{
exist_invalid_fd = false;
n = poll(&fds[0], fds.size(), 1000);//检查是否有可读的套接字
if(n < 0)
{
std::cerr << "poll is error" << std::endl;
if(errno == EINTR)
continue;
break;
}
else if(n == 0) continue;
//poll的缺点来了,不管有没有可读的,全都遍历一遍读取
for(size_t i = 0; i < fds.size(); ++i)
{
//事件可读
if(fds[i].events & POLLIN)
{
if(fds[i].fd == listenfd)//服务器套接字可读,客户端连接来了
{
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr_in));
socklen_t len = sizeof(struct sockaddr_in);
int clientsock = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
if(clientsock != -1)
{
int oldSocketFlag = fcntl(clientsock, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if(fcntl(clientsock, F_SETFL, newSocketFlag) == -1)
{
close(clientsock);
std::cerr<<"change client socket fail\n";
}
else
{
struct pollfd client_fd_info;
client_fd_info.fd = clientsock;
client_fd_info.events = POLLIN;
client_fd_info.revents = 0;
fds.push_back(client_fd_info);
std::cout << "new client socket create successful" << std::endl;
}
}
}
else//其他事件可读
{
char recvBuf[100] = "";
int m = recv(fds[i].fd, recvBuf, 100, 0);
if(m <= 0)
{
if(errno != EINTR && errno != EWOULDBLOCK)
{
for(std::vector<pollfd>::iterator iter = fds.begin();iter != fds.end(); ++iter)
{
if(iter->fd == fds[i].fd)
{
std::cout << "client diconnected, clientfd:" << fds[i].fd << std::endl;
close(fds[i].fd);
iter->fd = -1;
exist_invalid_fd = true;
break;
}
}
}
}
else
{
std::cout << "recv form client: " << recvBuf << ", clientfd: "<< fds[i].fd << std::endl;
}
}
}
else if(fds[i].revents & POLLERR)
{
std::cerr<< "revents is error\n";
}
}
if(exist_invalid_fd)
{
for(std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end();)
{
if(iter->fd == -1)
iter = fds.erase(iter);
else
++iter;
}
}
}
for(std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++iter)
close(iter->fd);
}
poll的缺点也很明显,第一,它会不管数组中的文件描述符是否发送了变化,全都从内核空间和用户空间之间整体复制;第二,poll函数返回之后,需要遍历fd集合来获取就绪的fd;第三,同时连接大量用户在某一时刻可能只有很少的就绪状态,因为随着监视的描述符数量的增长,其效率也会下降。
7.epoll模型
1.基本用法
(1).创建epollfd
int epoll_create(int size);
size传入一个大于0的参数就行了,成功创建则返回一个非负数的epollfd;
(2).将需要操作的fd进行操作,比如将fd绑定到epollfd上,或者解绑下来
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epfd:被创建出来的epollfd
op:操作类型,比如解绑,绑定到epfd;它的取值如下:
1).EPOLL_CTL_ADD绑定到epfd
2).EPOLL_CTL_MOD修改fd
3).EPOLL_CTL_DEL从epfd上解绑
fd:被监听的描述符
event:一个结构体地址,如果没有特殊要求可以设置为NULL
它的结构如下:
struct epoll_event{
uint_32_t events;//需要检测的事件
epoll_data_t data;//用户自定义的数据
};
成功调用返回0,失败返回-1
(3).设置好fd需要检测的事件,绑定好fd之后就可以开始调用epoll_wait函数来检测事件了
int epoll_wait(int epfd, struct epoll_event* events, int maxevent, int timeout);
epfd:需要检测描述符集合
events:监听的事件集合
maxevent:最大事件数量
timeout:监听的时间,毫秒
函数调用成功则需要返回返回事件fd的数量,返回0代表超时,返回-1代表失败。
2.epoll和poll的区别
epoll_wait调用结束后,可以得到所有事件就绪的套接字,而poll在调用结束后,得到了是原本全部的套接字,需要一个一个去检查谁才是可以用的。
3.边沿触发ET与水平触发LT
epoll有水平触发和边缘促发,这个概念很简单,不要说不懂。
水平触发:事件存在则不断触发
边沿触发:事件不存在的时候,突然有了则触发,如果事件还存在则不促发;如果事件没有了,然后再次出现,则触发。简单来讲,事件状态发生了改变就触发。
以socket读事件为例子
水平触发条件:socket上有可读的数据
边沿触发条件:socket上来了新的数据
socket写事件的例子
水平触发(LT):socket可写变成了不可写,或者时socket不可写变成了可写
边沿触发(ET):socket不可写变成了可写
那么对于读写过程就要慎重处理:
(1)读过程
如果socket上存在可读数据,使用边沿触发就要一次读取全部可读。
(2)写过程
如果socket上存在可写数据,使用水平触发,一定要一次把全部写数据写完。
上面这两点很好理解吧,因为如果在读过程中,边沿触发只进行一次,除非下来来新的数据,如果不一次读完,则会出现问题,也许是数据丢失。
cpp
#include<sys/types.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<poll.h>
#include<iostream>
#include<string.h>
#include<errno.h>
#include<vector>
int main(){
//创建套接字
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1)
{
std::cerr << "create socket error " << std::endl;
return -1;
}
//设置为非阻塞
int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if(fcntl(listenfd, F_SETFL, newSocketFlag)==-1)
{
close(listenfd);
std::cerr<<"scoket flag change error" <<std::endl;
}
int no = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*) &no, sizeof(no));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*) &no, sizeof(no));
//设置服务器的地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(3000);
//绑定服务器的地址
if(bind(listenfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) == -1)
{
std::cout << "绑定失败" << std::endl;
close(listenfd);
return -1;
}
//监听
if(listen(listenfd, 100) == -1)
{
close(listenfd);
return -1;
}
//创建一个epollfd
int epollfd = epoll_create(1);
if(epollfd == -1)
{
std::cerr << "create epollfd error\n";
close(listenfd);
return -1;
}
//事件的设置
epoll_event listen_fd_event;
listen_fd_event.data.fd = listenfd;//设置监听对象
listen_fd_event.events = EPOLLIN;//设置监听事件
//默认使用的是水平触发
//加入宏定义之后可以使用边沿触发
#ifdef __ET__
std::cout << "set ET " <<std::endl;
listen_fd_event.events |= EPOLLET;
#endif
//将listenfd绑定到epollfd上面
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
{
std::cerr<<"epoll_ctl error\n";
close(listenfd);
return -1;
}
int n;
while(true)
{
//检测事件,将监听的事件写入到下面这个数组中
epoll_event epoll_events[1024];
n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if(n < 0)
{
if(errno == EINTR)
continue;
std::cerr << "epoll_wait is error" << std::endl;
break;
}
//监听超时,继续监听
else if(n == 0)
continue;
//检测事件集合
for(size_t i = 0; i < n; ++i)
{
//如果事件被检测到可读
if(epoll_events[i].events & EPOLLIN)
{
if(epoll_events[i].data.fd == listenfd)//如果可读的事件是listenfd
{
//给连接到的客户端创建套接字,设置为非阻塞
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr_in));
socklen_t len = sizeof(struct sockaddr_in);
int clientsock = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
if(clientsock != -1)
{
int oldSocketFlag = fcntl(clientsock, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if(fcntl(clientsock, F_SETFL, newSocketFlag) == -1)
{
close(clientsock);
std::cerr<<"change client socket fail\n";
}
else
{
epoll_event client_fd_event;
client_fd_event.data.fd = clientsock;
client_fd_event.events = EPOLLIN;
#ifdef __EL__
client_fd_event.events |= EPOLLET;
#endif
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &client_fd_event))
{
std::cout << "new client socket create successful" << std::endl;
}
else
{
std::cerr << "client epoll ctl is error " << std::endl;
close(clientsock);
}
}
}
else//其他事件可读,每次读取一个字节。用来测试水平触发
{
char recvBuf[100] = "";
int m = recv(epoll_events[i].data.fd, recvBuf, 1, 0);
if(m == 0)
{
//对端关闭了连接,从epollfd上移除clientfd
if(epoll_ctl(epollfd,EPOLL_CTL_DEL,epoll_events[i].data.fd, NULL))
{
std::cout << "客户端断开连接" << epoll_events[i].data.fd << std::endl;
}
close(epoll_events[i].data.fd);
}
else if(m < 0)
{
//如果出错
if(errno != EINTR && errno != EWOULDBLOCK)
{
if(epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL)!=-1)
{
std::cout << "客户端断开连接" << epoll_events[i].data.fd << std::endl;
}
close(epoll_events[i].data.fd);
}
}
else
{
std::cout << "recv form client: " << recvBuf << ", clientfd: "<< epoll_events[i].data.fd << std::endl;
}
}
}
else if(epoll_events[i].events & EPOLLERR)
{
std::cerr<< "events is error\n";
}
}
}
close(listenfd);
return 0;
}
编译的时候可以加入不同的宏定义来测试,可以得知使用水平触发的时候,只要缓存区有数据,就会被不断读取。我们方式一个字符串到服务器,现在服务器每次只能读取一个字符,会发现水平触发的时候,它每次都读取一个字符,直到读取完毕,而加入宏定义__ET__之后,发送一个长字符串,它读取一个字符后就不动了,不读取了。
同样的,我们修改一下事件触发的方式,可以看看写模式下的两种触发方式
在这种写模式下,水平触发会不断发送内核缓存可写的信号,导致程序疯狂输出。
总结:在ET模式下,服务器给客服端注册可写模式后,在经过一次触发后,除非新的数据到达,否则不会被触发,如果需要在缓存区有数据的时候再次触发,则需要再次注册,检测可写事件。在ET模式下,读事件时必须把数据读取干净,否则下一次事件发送过来,就没有机会读上一次没读完的数据了。在LT模式下,不需要写事件的时候,一定要移除对他的注册
4.EPOLLONESSHOT选项
如果epoll模型注册了这个模式,则被监听的对象在触发一次之后,再也不会被触发
8.readv和writev
高效的服务要减少系统调用,而write和read都是系统调用,他们读取文件描述符对应的缓冲区,然后将缓冲区的数据写入到其他地方,这样就需要频繁进行系统调用,我们也可以一次性的把其他缓冲区的内容放在一起,按照自定义的规则进行一次系统调用。这个方法就是如下几个函数:
ssize_t readv(int fd, const struct iovec* iov, int iovcnt);
ssize_t writev(int fd, const struct iovec* iov, int iovcnt);
ssize_t preadv(int fd, const struct iovec* iov, int iovcnt, off_t offset);
ssize_t pwritev(int fd, const struct iovec* iov, int iovcnt, off_t offset);
fd代表文件描述符
offset代表平移指针位置,也就是从某个地方开始读取或者写入
iov是一个结构体指针,用来指向一个结构体数据,它是用来连接多个需要一起连接到fd的对象组
iovcnt代表数组元素个数.
返回值为读取或者写入的字节数
struct iovcnt{
void* iov_base;//数据起始位置
size_t iov_len;//需要传输的字节数
};
cpp
char *buf1[] = "第一个缓存区11111111111111";
char *buf2[] = "第二个缓冲区22222222222222";
char *buf3[] = "第三个缓冲区33333333333333";
struct iovec iov[3];
iov[0].iov_base = buf1;
iov[0].iov_len = strlen(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = strlen(buf2);
iov[2].iov_base = buf3;
iov[2].iov_len = strlen(buf3);
size_t nwirtelen = writev(fpCSV, iov, 2);
9.本机字节序和网络字节序
本机字节序分为小端和大端两种,小端指的是一个整数,它的高位被储存在高位地址,低位储存在低位地址,大端相反。网络字节序是网络传输过程使用的字节序,他用的是大端。
给一个例子
比如一个数字位0x1234
小端字节序上,如果我们将他转换为char类型,然后作为整数输出,得到的是12
大端字节序就是34