文章目录
1 - server socket
首先要创建server端的socket(socket函数),绑定端口(bind函数),开始监听(listen函数)
- 绑定端口时注意,0-1023为知名端口号范围,能够bind的端口限制在1024-65535之间
- 还要注意绑定的端口如果被占用(Address already in use),就要设置socket选项
int setsockopt(int s, int level, int optname, const void * optval, ,socklen_toptlen);
//s:要设置状态的socket
//level:参数level 代表欲设置的网络层, 一般设成SOL_SOCKET
//optname:代表欲设置的选项,我们设置成 SO_REUSEADDR 允许在bind ()过程中本地地址可重复使用
socket创建、绑定、监听
int socket_bind_listen(int port)
{
// 检查port值,取正确区间范围
if (port < 1024 || port > 65535)
return -1;
// 创建socket(IPv4 + TCP),返回监听描述符
int listen_fd = 0;
if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
return -1;
// 消除bind时"Address already in use"错误
int optval = 1;
if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
return -1;
// 设置服务器IP和Port,和监听描述符绑定
struct sockaddr_in server_addr;
bzero((char*)&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons((unsigned short)port);
if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
return -1;
// 开始监听,最大等待队列长为LISTENQ
if(listen(listen_fd, LISTENQ) == -1)
return -1;
// 无效监听描述符
if(listen_fd == -1)
{
close(listen_fd);
return -1;
}
return listen_fd;
}
关于listen函数
int listen(int socket, int backlog);
listen函数通常应该在调用socket函数和bind函数之后,并在调用accept函数之前
listen函数仅由TCP服务调用,它做两件事
- 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求。
- listen函数的第二个参数backlog规定了内核应为第一个参数socket排队的最大连接个数
为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:
- (1)未完成连接队列:内含处于SYN_RCVD状态的client
- (2)已完成连接队列:内含处于ESTABLISHED状态的client
listen函数的backlog参数曾被规定为这两个队列总和的最大值
设置监听socket为非阻塞
///将socket设置为非阻塞,成功->0 失败->-1
int setSocketNonBlocking(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);//获得文件状态标记
if(flag == -1)
return -1;
flag |= O_NONBLOCK;
if(fcntl(fd, F_SETFL, flag) == -1)
return -1;
return 0;
}
//关于fcntl函数的详细使用 见参考【Linux 文件描述符设置为非阻塞的方法】
为什么要将socket设置为非阻塞呢?
因为套接字的默认状态是阻塞的。这就意味着当调用一个不能立即完成的socket函数时,调用该函数的进程将进入睡眠(即阻塞),等待相应操作完成后,才会执行该函数之后的语句。
可能阻塞的socket调用分为以下四种:
-
读:包括read 、 readv、 recv、 recvfrom和 recvmsg共5个函数。
- 阻塞模式下调用read(),recv()等读套接字函数会一直阻塞住,直到有数据到来才返回。当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数。当sockt的接收缓冲区中的数据大于期望读取的字节数时,读取期望读取的字节数,返回实际读取的长度。
- 对于非阻塞socket而言,socket的接收缓冲区中有没有数据,read调用都会立刻返回。接收缓冲区中有数据时,与阻塞socket有数据的情况是一样的,如果接收缓冲区中没有数据,则返回错误号为EWOULDBLOCK,表示该操作本来应该阻塞的,但是由于本socket为非阻塞的socket,因此立刻返回。遇到这样的情况,可以在下次接着去尝试读取。如果返回值是其它负值,则表明读取错误。
-
输出:包括write、writev、send、sendto和sendmsg共5个函数。
- 对于阻塞Socket而言,如果发送缓冲区没有空间或者空间不足的话,write操作会直接阻塞住,如果有足够空间,则拷贝所有数据到发送缓冲区,然后返回.
- 对于写操作write,原理和read是类似的,非阻塞socket在发送缓冲区没有空间时会直接返回错误号EWOULDBLOCK,表示没有空间可写数据,如果错误号是别的值,则表明发送失败。如果发送缓冲区中有足够空间或者是不足以拷贝所有待发送数据的空间的话,则拷贝前面N个能够容纳的数据,返回实际拷贝的字节数。
-
接受连接:即accept函数。
- 阻塞模式下调用accept()函数,而且没有新连接时,进程会进入睡眠状态,直到有可用的连接,才返回。
- 非阻塞模式下调用accept()函数立即返回,有连接返回客户端套接字描述符,没有新连接时,将返回EWOULDBLOCK错误码,表示本来应该阻塞。
-
发起连接:connect函数。
- 阻塞方式下,connect首先发送SYN请求到服务器,当客户端收到服务器返回的SYN的确认时,则connect返回,否则的话一直阻塞。
- 非阻塞方式,connect将启用TCP协议的三次握手,但是connect函数并不等待连接建立好才返回,而是立即返回,返回的错误码为EINPROGRESS,表示正在进行某种过程。
参考:
2 - 设置epoll事件
epoll事件类型
首先介绍一下EPOLL中的事件:
EPOLLIN :读事件(连接到达;有数据来临)
EPOLLOUT:写事件(有数据要写)
EPOLLET:设置事件为边沿触发
EPOLLONESHOT:只触发一次,事件自动被删除
EPOLLPRI:带外数据,与select的异常事件集合对应
EPOLLERR:错误事件
epoll在一个文件描述符上只能有一个事件,在一个描述符上添加多个事件,会产生EEXIST的错误。同样,删除epoll的事件,只需描述符就够了
参考:epoll事件
水平触发与边沿触发
水平触发(LT,level trigger)
- 对于读操作:只要输入缓冲内容不为空,LT模式返回读就绪。
- 对于写操作:只要输出缓冲区还不满,LT模式会返回写就绪。
边沿触发(ET,edge trigger)
- 对于读操作:只在以下几个条件其中之一被满足时,才会触发返回读就绪操作
- 当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候
- 当有新数据到达时,即缓冲区中的待读数据变多的时候
- 当缓冲区有数据可读,且应用进程对相应的描述符进行
EPOLL_CTL_MOD
修改EPOLLIN
事件时
- 对于写操作:只在以下几个条件其中之一被满足时,才会触发返回写就绪操作
- 当缓冲区由不可写变为可写时
- 当有旧数据被发送走,即缓冲区中的内容变少的时候
- 当缓冲区有空间可写,且应用进程对相应的描述符进行
EPOLL_CTL_MOD
修改EPOLLOUT
事件时
总结一下:
水平触发:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!
边沿触发:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!
参考:
3 - 注册新描述符
将server socket注册到epoll内核事件表中。下面代码中传入参数:
- epoll_fd:内核事件表
- fd:server socket
- request:发生事件的具体内容
- events:事件类型
// 注册新描述符
int epoll_add(int epoll_fd, int fd, void *request, __uint32_t events)
{
struct epoll_event event;
event.data.ptr = request;
event.events = events;
//printf("add to epoll %d\n", fd);
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) < 0)
{
perror("epoll_add error");
return -1;
}
return 0;
}
复习一下epoll_event的结构:
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 events(也就是事件) */
epoll_data_t data; /* User data variable */
因为epoll_event.data.ptr是一个void指针,所以传入的请求request也必须在传入前转换为void指针
补充一下void指针与static_cast函数的知识:
void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针对 void 指针赋值
int *a; void *p; p=a;
如果要将 void 指针 p 赋给其他类型的指针,则需要强制类型转换,就本例而言:a=(int *)p。
内存分配函数 malloc 函数返回的指针就是 void * 型,用户在使用这个指针的时候,要进行强制类型转换,也就是显式说明该指针指向的内存中是存放的什么类型的数据 (int *)malloc(1024) 表示强制规定 malloc 返回的 void* 指针指向的内存中存放的是一个个的 int 型数据。
static_cast <type-id>( expression )
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
4 - 等待事件发生
写一个自己的my_epoll_wait函数,等待事件发生
// 返回活跃事件数
int my_epoll_wait(int epoll_fd, struct epoll_event* events, int max_events, int timeout)
{
int ret_count = epoll_wait(epoll_fd, events, max_events, timeout);
if (ret_count < 0)
{
perror("epoll wait error");
}
return ret_count;
}