socket编程与libevent2的一些概括总结

PS: 更全面的总结能够从相关的文档中获取,为了叙述方便这里特指linux环境下linux

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的情形:并发

  1. 链接失败
    block:当即返回,返回值-1,同时设置errno := ENOTCONN
    nonblock: 同上dom

  2. 缓冲区中有数据:
    block: 当即返回,将缓冲区的数据写入buf,最多写入len字节,返回值为写入的字节数
    nonblock: 同上异步

  3. 缓冲区无数据:
    block:将阻塞等待缓冲区有数据
    nonblock:当即返回,返回值-1,同时设置errno := EAGAINsocket

相似的,对于send(), connect(), bind(), accept(),均有相似同样的区别函数

设置

有以下方式设置nonblock高并发

  1. 新建 socket 时设置
    在传入 socket type 时,同时置SOCK_NONBLOCK位为1spa

    sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
  2. 使用fcntl()设置线程

    int flag = fcntl(sock, F_GETFL);
    fcntl(sock, F_SETFL, flag | O_NONBLOCK);
  3. 使用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 addr0.0.0.0,将再也不表示某一个具体的地址,而是表示本地的全部的可用地址。

reuse有三个级别:

  1. non-reuse: src addrsrc port不能冲突(同一个protocol下), 0.0.0.0和其余IP视为冲突

  2. reuse-addr: src addrsrc port不能冲突(同一个protocol下), 0.0.0.0和其余IP视为不冲突

  3. reuse-port: src addrsrc port能够冲突

下边仍然举例说明reuse的特性
系统有两个网口,分别是192.168.0.10110.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.0192.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.0192.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

  1. 使用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));
  2. 使用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 - 本地socket

  • AF_INET - ipv4

  • AF_INET6 - ipv6

type 通常设置为:

  • SOCK_STREAM - TCP

  • SOCK_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生命周期

用户本身建立的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 */);

res是要手动指派的flag

相关文章
相关标签/搜索