目录
快速开始
TCP/IP通信模型
Server & Client
函数详解
socket()
int socket(int domain, int type, int protocol);
- 返回值:获得I/O文件描述符(类似于open())
- 参数:
- domain:即协议域,又称为协议族(family)
- AF_INET,使用IPV4地址
- AF_INET6,使用IPV6地址
- AF_LOCAL(或称AF_UNIX),使用一个绝对路径名作为地址
- AF_ROUTE等等
- type:指定socket类型。
- SOCK_STREAM,传输控制协议TCP
- SOCK_DGRAM,用户报数据协议UDP
- SOCK_RAW,原始数据(IP协议的数据报),用于直接访问网络层
- SOCK_PACKET,SOCK_SEQPACKET等等
- protocol:协议,通常是零
- domain:即协议域,又称为协议族(family)
bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 返回值:为0时表示绑定成功,-1时表示绑定失败
- 参数:
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket
- addr:指向要绑定给sockfd的协议地址,根据创建socket时的地址协议族的不同而不同;
- protocol:协议,通常是零
补充:字节序
uint32_t ntohl (uint32_t __netlong);
uint16_t ntohs (uint16_t __netshort);
uint32_t htonl (uint32_t __hostlong);
uint16_t htons (uint16_t __hostshort);
函数名:n代表网络,h代表主机,即n to h,最后的 l 代表整型,4字节,s代表2字节。
struct sockaddr {
__SOCKADDR_COMMON (sa_);
char sa_data[14]; /* Address data. */
};
IPV4
IPV6
如何使用?
if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
listen()
int listen(int sockfd, int backlog);
- 返回值:若成功返回0,若出错返回-1
- 参数:
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket
- backlog:完全连接队列容量
connect()
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
- 返回值:若成功返回0,若出错返回-1
- 参数:
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket
- serv_addr:server端地址
- addr_len:地址结构体长度
连接过程:
当全连接队列已满,就会根据/proc/sys/net/ipv4/tcp_abort_on_overflow策略进行处理。
- 当tcp_abort_on_overflow=0,服务端直接丢弃该ACK;
- 此时服务端处于【syn_rcvd】的状态
- 客户端处于【established】的状态
- 服务端定时 SYN/ACK 给客户端(默认5次)
- 客户端发送数据过来,服务端会返回RST
- 当tcp_abort_on_overflow=1,服务端直接返回RST通知client;
- client会报connection reset by peer
accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
send()/recv()
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 返回值:
- -1:出错,表示通信链路已不可用
- 大于零的值:表示已发送或接收数据的长度
- 参数:
- sockfd:为已建立好连接的socket。
- buf:收发的数据地址
- len:数据的长度,不能超过buf的大小,否则内存溢出
- flags填0, 其他数值意义不大
发的时候可能没有全发,所以实际开发过程中需要在外进行封装,记录发了多少字节,接收也同理。
拆包和粘包
- 影响:
- 接收端难以分辨Message
- 原因:
- socket缓冲区与滑动窗口
- MSS/MTU限制
- Nagle算法
- 措施:
- 协议:定长、特殊分隔符、变长
close()四次分手
- MSL(Maximum Segment Lifetime),报文最大生存时间;
- 2MSL默认是60秒,停留在TIME_WAIT状态 60 秒;
- 即断开连接后端口有60秒是不可用的。
并发编程
多进程模式
fork()一个子进程,给每个连接提供一个子进程。
多线程模式
I/O复用
服务端经典的C10k问题
- 五种I/O模型
- 同步阻塞IO(Blocking IO):即传统的IO模型。
- 同步非阻塞IO(Non-blocking IO):设置socket属性为NONBLOCK。
- IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO
- 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO
基于信号驱动的IO(Signal Driven IO)模型,该模型并不常用
- 同步和异步、阻塞和非阻塞
- 同步和异步描述的是:用户线程与内核的交互方式
- 阻塞和非阻塞描述的是:用户线程调用内核IO操作的方式
- 同步和异步关注的是:程序之间的协作关系
- 阻塞与非阻塞:单个进程(线程)的执行状态
select()
思路:在读取socket之前,先查下它的状态,然后只处理ready状态的socket。
// 初始化fdset集合
fd_set readfdset;
FD_ZERO(&readfdset);
FD_SET(listensock,&readfdset);
// readfdset中socket的最大值。
int maxfd = listensock;
- fd_set: 1024bit的位图
- FD_SET: 将fd对应的位设置为1
- maxfd:监视fd中的最大值
- readfdset保存到临时变量
- 调用select阻塞等待I/O就绪
- 遍历readfdset,逐位检测
- listen事件,调用accept获取新的fd
- read事件
优点:
- 单线程处理多个的I/O请求
- 可以设置超时时间
缺陷:
- 可监视的fd数量上限是1024
- 线性扫描fd,效率低
poll()
思路:用于数组代替fd_set,消除监视fd数量的限制。
struct pollfd fds[MAXNFDS];
fds[clientsock].fd = clientsock;
fds[clientsock].events = POLLIN;
- 定义任意长度pollfd数组
- 以fd为下标,填充数组,并指定需要监听的I/O类型
struct pollfd {
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
使用:
while (1) {
int infds = poll(fds,maxfd+1,5000);
for (int eventfd=0; eventfd <= maxfd; eventfd++) {
if ((fds[eventfd].revents&POLLIN)==0) continue;
ssize_t isize=read(eventfd,buffer,sizeof(buffer));
// do something
}
}
优点:解决了selecr中fd上限的问题
缺陷:与select一样,遍历获取描述符
epoll()
思路:只返回状态发生变化的fd。
int epollfd = epoll_create(1);
// 添加监听描述符事件
struct epoll_event ev;
ev.data.fd = listensock;
ev.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,listensock,&ev);
基于事件的触发模式,事件类型:EPOLLIN、EPOLLOUT等。
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}
事件中可以携带fd或任意用户数据。
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
使用:
while (1) {
struct epoll_event events[64];
int infds = epoll_wait(epollfd,events,64,-1);
// 遍历有事件发生的结构数组。
for (int i=0;i<infds;i++) {
if (events[ii].events & EPOLLIN) {
// 读取客户端的数据。
read(events[i].data.fd,buffer,sizeof(buffer));
do_something()
}
}
}
高效的原因:
- 仅返回状态变化的fd
- 使用mmap,在内核与用户空间传递信息
使用限制:
- 当监控fd数量较少(10)时没有优势;
- 仅能在linux上使用;
设计模式reactor
- 同步的等待多个事件源到达
- 将时间多路分解以及分配相应的时间服务进行处理,这个分派采用server集中处理(dispatch)
- 分解的事件以及对应的事件服务应用从分派服务中分离出去(handler)
单reactor 单线程模型
此种模型,通常只有一个epoll对象,所有的接收客户端连接、客户端读取、客户端写入操作都饱含在一个线程内。该种模型也有一些中间件在用,比如redis。
单reactor 多线程模型
该模型主要是通过将前面的模型进行改造,将读写的业务逻辑交给具体的线程池来实现,这样可以显示reactor线程对IO的响应,以此提升系统性能。
muti-reactor 多线程/进程模型
在这种模型中,主要分为两个部分:mainReactor、subReactors。
mainReactor主要负责接收客户端的连接,然后将建立的连接通过负载均衡的方式分发给subReactors,subReactors来负责具体的每个连接的读写。
主流中间件所采用的网络模型
组件框架 | epoll触发方式 | reactor模型 |
---|---|---|
redis | 水平触发 | 单reactor模型 |
skynet | 水平触发 | 单reactor模型 |
memcached | 水平触发 | 多reactor模型(多线程) |
nginx | 边缘触发 | 多reactor模型(多进程) |
主流网络框架
- netty
- gnet
- libevent
- evio(golang)
- ACE(C++)
- boost::asio(C++)
- muduo(linux only)