1. 涉及的一些背景知识
1.1. nonblock socket
描述
对应block,如果一个socket设置为nonblock,那么其相关的操作将变为非阻塞的。这里所说的非阻塞,并不是说异步回调什么的,例如,调用recv()
函数:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
read = recv(sock, buf, len, 0);
如果是默认的block情形,这个函数将一直等待直到获取到数据,或者报错。在高并发中,这显然是悲剧的。
如果设置为noblock,同样的调用将直接返回。
下边详细描述一下的recv的情形:
- 连接失败
block:立即返回,返回值-1,同时设置errno := ENOTCONN
nonblock: 同上 - 缓冲区中有数据:
block: 立即返回,将缓冲区的数据写入buf,最多写入len字节,返回值为写入的字节数
nonblock: 同上 - 缓冲区无数据:
block:将阻塞等待缓冲区有数据
nonblock:立即返回,返回值-1,同时设置errno := EAGAIN
类似的,对于send()
, connect()
, bind()
, accept()
,均有类似一样的区别
设置
有如下方式设置nonblock
-
新建 socket 时设置
在传入 socket type 时,同时置SOCK_NONBLOCK
位为1sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
-
使用
fcntl()
设置int flag = fcntl(sock, F_GETFL); fcntl(sock, F_SETFL, flag | O_NONBLOCK);
-
使用even2设置
#inlcude <event2/util.h> int evutil_make_socket_nonblocking(evutil_socket_t sock);
1.2. reuseable socket
描述
一个socket在系统中的表示如下
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
如果指定src addr
为0.0.0.0
,将不再表示某一个具体的地址,而是表示本地的所有的可用地址。
reuse有三个级别:
non-reuse
:src addr
和src port
不能冲突(同一个protocol
下),0.0.0.0
和其他IP视为冲突reuse-addr
:src addr
和src port
不能冲突(同一个protocol
下),0.0.0.0
和其他IP视为不冲突reuse-port
:src addr
和src port
可以冲突
下边仍然举例说明reuse
的特性
系统有两个网口,分别是192.168.0.101
和10.0.0.101
。
- 情形1:
sock1绑定了192.168.0.101:8080
,sock2尝试绑定10.0.0.101:8080
non-reuse
- 可以绑定成功,虽然端口一样,但是addr不同
reuse
- 同上 - 情形2
sock1绑定了0.0.0.0:8080
, sock2尝试绑定192.168.0.101:8080
non-reuse
- 不能绑定成功,系统认为0.0.0.0
包含了所有的本地ip,发生冲突
reuse
- 可以绑定成功,系统认为0.0.0.0
和192.168.0.101
不是一样的地址 - 情形3
sock1绑定了192.168.0.101:8080
,sock2尝试绑定0.0.0.0:8080
non-reuse
- 不能绑定成功,系统认为0.0.0.0
包含了所有的本地ip,发生冲突
reuse
- 可以绑定成功,系统认为0.0.0.0
和192.168.0.101
不是一样的地址 - 情形4
sock1绑定了0.0.0.0:8080
,sock2尝试绑定0.0.0.0:8080
non-reuse
- 不能绑定成功,系统认为0.0.0.0
包含了所有的本地ip,发生冲突
reuse-addr
- 不能绑定成功,系统认为0.0.0.0
包含了所有的本地ip,发生冲突
reuse-port
- 可以绑定成功
设置reuse
-
使用
setsockopt()
必须设置所有相关的sock。
设置reuse-addr:setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
设置reuse-port:
setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int));
-
使用event2设置
#inlcude <event2/util.h> int evutil_make_listen_socket_reuseable(evutil_socket_t sock);
2. 常用的系统API接口
新建一个socket
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain
一般设置为:
AF_UNIX
- 本地socketAF_INET
- ipv4AF_INET6
- ipv6
type
一般设置为:
SOCK_STREAM
- TCPSOCK_DGRAM
- UDP
连接到远程端口
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
对于不同协议,addr
的类型不同,长度也不同,这里需要把不同的类型强转为struct sockaddr *
,在强转中,addr
的类型信息丢失,所以需要在addrlen
中指定原有类型的长度。
绑定到本地端口
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
addr
类似connect()
,这个函数常用语服务器端,但是实际上客户端也是可以使用的(然并卵一般没啥意义)
读写数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
读写数据涉及的问题较多,第一是失败时候返回-1而不是0,如果是0表示socket关闭。第二就是读写不一定100%完成,计划读写512字节,但是读到256字节的时候发生了中断或者没有数据/空闲缓冲区都是是可能的,返回值表示实际读入和写出的字节数。
监听数据
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
和主动发起连接不同,被动接收连接分为三个阶段,bind()
用来设置本地端口,listen()
表示socket开始接收到来的连接,而不会建立连接,要真正建立连接,使用accept()
关闭连接
#include <unistd.h>
int close(int fd);
关闭即可,没啥说的
3. 常用的event2的接口
旧版libevent中,一般只能操作一个全局的event_base,而在新版libevent中,event_base交由用户来管理,用户可以创建删除event_base,也可以把event注册到不同的event_base上。
新建一个 event_base
#include <event2/event.h>
struct event_base *event_base_new(void);
释放一个event_base
#include <event2/event.h>
void event_base_free(struct event_base *eb);
event的生命周期
event的生命周期与相关的函数关系密切
用户自己创建的event是uninitialized
的,需要使用event_assign()
进行初始化,或者直接使用event_new()
从无到有创建一个新的初始化了的event。在初始化时,完成了回调函数的绑定。
event的初始状态是non-pending,表示这个event不会被触发。
新建(并初始化)一个 event
struct event *event_new(struct event_base *base, evutil_socket_t fd, short events,
event_callback_fn callback, void *callback_arg);
新建event需要给定event_base, evutil_socket_t
与系统相兼容,在linux下实际就是int,与socket()返回的类型一致
#ifdef WIN32
#define evutil_socket_t intptr_t
#else
#define evutil_socket_t int
#endif
events是一组flag,用于表示要监视的事件类型,还会影响event的一些行为,包括:
EV_TIMEOUT
- 监视超时的事件
需要说明的是,在调用event_new()
时,这个flag是不用设置的,如果event发生超时,则必然会触发,无论设置与否EV_READ
- 监视可读的事件EV_WRITE
- 监视可写的事件EV_SIGNAL
- 监视信号量EV_PERSIST
- 永久生效,否则触发一次后就失效了EV_ET
- 设置边缘触发(edge-triggered)
callback和callback_arg是回调操作所需的,不再详述
新建的event是non-pending状态的
初始化一个event
int event_assign(struct event *ev,
struct event_base *base, evutil_socket_t fd, short events,
event_callback_fn callback, void *callback_arg);
这个不会申请内存,其他同event_new()
释放一个event
void event_free(struct event *ev);
判断event是否初始化/被释放
int event_initialized(const struct event *ev);
将event置为pending状态
int event_add(struct event *ev, const struct timeval *timeout);
其中timeout可以指定超时时间,超时和EV_TIMEOUT配合使用。如果timeout如果为NULL,则表示永不超时,struct timeval的结构为:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
额外说句,操作当前时间对应的timeval可以用
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
int settimeofday(const struct timeval *tv, const struct timezone *tz);
将event置为non-pending状态
int event_del(struct event *ev);
检查event是否为pending状态
int event_pending(const struct event *ev, short events, struct timeval *tv);
需要注意的是,不需要查询event是否为active状态,因为在active时,线程正在执行回调函数,其他函数需要等到回调执行完毕,而此时已经退出了active状态
将event置为active状态
void event_active(struct event *ev, int res, short/* deprecated */);
ev);
## 检查event是否为pending状态
int event_pending(const struct event *ev, short events, struct timeval *tv);
需要注意的是,不需要查询event是否为active状态,因为在active时,线程正在执行回调函数,其他函数需要等到回调执行完毕,而此时已经退出了active状态
## 将event置为active状态
void event_active(struct event ev, int res, short/ deprecated */);
`res`是要手动指派的flag
想了解学习更多C++后台服务器方面的知识,请关注:
微信公众号:C++后台服务器开发