一、socket
socket 网络套接字
一个文件文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现(接收缓冲区和发送缓冲区))
通讯过程中,套接字一定是 【成对】 出现的。
二、网络字节序
小端法(PC本地存储): 高位存高地址。低位存低地址。
大端法(网络存储): 高位存低地址。低位存高地址。
涉及的四个函数:
man htonl
1.NAME
htonl, htons, ntohl, ntohs - convert values between host and network byte order
2.SYNOPSIS
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //本地->网络(IP) converts the unsigned integer hostlong from host byte order to network byte order.
//这里的IP指的不是192.168.1.1这样的字符串形式
uint16_t htons(uint16_t hostshort); //本地->网络(Port) converts the unsigned short integer hostshort from host byte order to network byte order.
uint32_t ntohl(uint32_t netlong); //网络->本地(IP) converts the unsigned integer netlong from network byte order to host byte order.
uint16_t ntohs(uint16_t netshort); //网络->本地(Port) converts the unsigned short integer netshort from network byte order to host byte order.
三、IP地址转换函数
主机字节序(小端)和网络字节序(大端)相互转换时,需要用到此节提到的转换函数。
1.inet_pton()//本地字节序(string IP)-> 网络字节序
//客户端connect()函数会用到
man inet_pton
1)NAME
inet_pton - convert IPv4 and IPv6 addresses from text to binary form
2)SYNOPSIS
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
3)PARAMETER
af:指定IP协议
AF_INET IPv4
AF_INET6 IPv6
src:传入IP地址(点分十进制,192.168.1.1)
dst:值结果参数,传出转换后的网络字节序 IP 地址。
4)RETURN VALUE
inet_pton() returns
1 on success (network address was successfully converted).
0 is returned if src does not contain a character string repre‐senting a valid network address in the specified address family. If af does not contain a valid address family,
-1 is returned and errno is set to EAFNOSUPPORT.
2.inet_ntop() //网络字节序-> 本地字节序(string IP)
//服务端accept()函数会用到
1)NAME
inet_ntop - convert IPv4 and IPv6 addresses from binary to text form
2)SYNOPSIS
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
3)PARAMETER
af:指定IP协议
AF_INET IPv4
AF_INET6 IPv6
src: 传入网络字节序 IP 地址
dst:值结果参数,传出转换后的IP地址(点分十进制,192.168.1.1)。
4)RETURN VALUE
On success, inet_ntop() returns a non-null pointer to dst.
NULL is returned if there was an error, with errno set to indicate the error.
四、sockaddr 和 sockaddr_in 地址结构体
-
sockaddr 和 sockaddr_in 区别
大小相同,都是16字节。 区别在于:sockaddr 诞生日期早,底层封装应用的多。 sockaddr_in 后来诞生,更精细化,现在更常用。 故现在使用的都是 sockaddr_in,但在bind(),accept()时都需要将sockaddr_in强转为sockaddr才行 。
-
sockaddr_in结构体详解
man 7 ip //查看 sockaddr_in 信息 struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
-
实际应用
man 7 ip //查看 sockaddr_in 信息 实际应用步骤如下: 1.定义并赋值 sockaddr_in 结构体 2.将 socketaddr_in 强转为 sockaddr 指针类型。 示例: struct sockaddr_in addr; addr.sin_family = AF_INET; //AF_INET6 addr.sin_port = htons(8080); /* 方式 1,这种不常用。 inet_pton(AF_INET,"10.219.10.193",(void*)&addr_s.sin_addr); 或者 int dst; inet_pton(AF_INET,"192.168.1.1",(void*)dst); addr.sin_addr.s_addr = dst; */ //方式2 ,这是常用方式。 addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 取系统中有效的任意IP地址,二进制类型。 bind(fd,(struct sockaddr*)addr,size);
五、服务端和客户端通讯
一般通讯由 3 个描述符组成。
一对用于客户端和服务器通讯,一个用于监听。
listen()函数的作用是设置监听上线(同一时刻接收到连接的个数),而不是设置监听。
accept()函数才是阻塞监听客户端连接。
accept()接收到客户端connect()连接后会生成一个新的socket用于通讯,而accept()调用的 fd 会返回继续进行监听。
客户端在connect()前没有显示bind()绑定客户端地址,那采用的就是“隐式绑定”。
六、TCP网络编程涉及函数
-
socket
man 2 socket 1.NAME socket - create an endpoint for communication 2.SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); 3.PARAMETER domain: 常用的有3个: AF_UNIX //Local communication AF_INET //IPv4 Internet protocols AF_INET6 //IPv6 Internet protocols type: 常用的有2个: SOCK_STREAM //流 TCP SOCK_DGRAM //报文 UDP protocol:默认 0 即可。 //所选用协议的代表协议是什么,也就是type对应的协议。0就是默认的指代。默认:SOCK_STREAM -> TCP ;SOCK_DGRAM -> UDP. 4.RTETURN VALUE On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
-
bind
man 2 bind 1.NAME bind - bind a name to a socket 2.SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 3.PARAMETER sockfd: socket函数返回值 struct sockaddr_in addr; addr.sinfamily = AF_INET; addr.sin_port = htons(8888); addr.sin_addr.s_addr = htonl(INADDR_ANY); //系统自行选择IP inet_pton(AF_INET,"10.219.10.193",(void*)&addr_s.sin_addr); //指定IP addr:(struct sockaddr *)&addr addrlen:sizeof(addr) 4.RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
-
listen
//设置同时与服务器建立连接的上线数 (同时进行三次握手的客户端数量) man 2 listen 1.NAME listen - listen for connections on a socket 2.SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); 3.PARAMETER sockfd: socket 函数返回值 backlog:上线数。最大值为 128 4.RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
-
accept
//阻塞等待客户端连接 man 2 accept 1.NAME accept, accept4 - accept a connection on a socket 2.SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 3.PARAMETER sockfd :socket函数返回值(也必须是listen过的fd) addr :值结果参数。传出成功与服务器建立连接的客户端地址结构 addrlen :传入传出参数。 传入:传入时addr的大小。一般为:sizeof(addr) 传出:客户端addr的实际大小。 4.RETURN VALUE On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket. On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
-
connect
//使用创建的socket与服务器连接 man 2 connect 1.NAME connect - initiate a connection on a socket 2.SYNOPSIS #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 3.PARAMETER sockfd :socket函数返回值 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&addr.sin_addr.s_addr); addr :传入参数。服务器地址结构。(也就是要连接的服务器的地址结构) 用inet_pton()给地址赋值。 addrlen :服务器地址结构大小。 4.RETURN VALUE If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately. 注: 如果connect前未使用bind显示绑定客户端地址,采用的就是“隐式绑定”。
-
实现简单的服务器和客户端DEMO
DEMO流程如下图所示:
DEMO:
https://github.com/Panor520/LinuxCode/tree/master/socket/tcp/simpledemo
七、UDP网络编程涉及函数及DEMO
-
socket
同 tcp中的socket -
recvfrom
man 2 recvfrom 1.NAME recv, recvfrom, recvmsg - receive a message from a socket 2.SYNOPSIS #include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 3.PARAMETER sockfd :通信的套接字 buf :缓冲区地址 //char buf[1024]; len :缓冲区大小 //sizeof(buf) flags : 默认0. src_addr:(struct addr*)&addr_c 传出,对端地址结构 //不需要对端地址信息可置 0 addrlen :传入传出参数。 socklen_t len_addr_c; //不需要对端地址信息可置 0 4.RETURN VALUE 成功 :成功接收数据字节数。 失败 :-1 errno 0 :对端关闭。
-
sendto
man 2 sendto
1.NAME
send, sendto, sendmsg - send a message on a socket
2.SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
3.PARAMETER
sockfd :通信的套接字
buf :缓冲区地址 //char buf[1024];
len :缓冲区大小 //sizeof(buf)
flags :默认0.
dest_addr:(struct addr*)&addr 传入,对端地址结构
addrlen :sizeof(addr).
4.RETURN VALUE
成功 :成功写如数据字节数
失败 :-1. errno
- UDP通信DEMO
server连接地址
client连接地址
八、优化——自定义封装通讯函数
八、read函数返回值(必须掌握)
read函数返回值:
>0 :实际读到的字节数
=0 :已经读到结尾(对端已经关闭)【重!点!,必须掌握】
-1 :进一步判断errno的值
errno = EAGAIN or EWOULDBLOCK : 设置了非阻塞方式 读。没有数据到达
errno = EINTR 慢速系统调用被 中断。
errno = “其他情况” 异常。
九、多进程并发服务器
十、多线程并发服务器
十一、服务端端口和地址复用(设置套接字选项避免2MSL时长报错问题)
先关闭服务端,主动关闭端就会经历 TIMEWAIT 状态(2MSL时长),立即再次启动服务端时会出现bind server error的错误,
可以用下面方法避免。
详见 unix网络编程 第7章
man setsockopt
先前关闭的服务端还在经历 TIMEWAIT状态,只是 端口和地址可以复用。
用法:
在服务端的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
十二、close和shutdown区别
当使用dup2()指定多个整数指向同一个文件描述符时:
close() 关闭只会关闭指定的那个。【单个关闭】
shutdown()关闭时,会将所有指向该文件描述符的连接全部关闭。【全关闭】
man 2 shutdown
1.NAME
shutdown - shut down part of a full-duplex connection
2.SYNOPSIS
#include <sys/socket.h>
int shutdown(int sockfd, int how);
3.PARAMETERS
how:
SHUT_RD 关读端
SHUT_WR 关写端
SHUT_RDWR 两端都关闭
4.RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
十三、多路I/O转接 服务器(select、poll)
-
基础
传统的 多线程和多进程服务器 就是是【阻塞】的。 而 多路I/O转接(select、poll、epoll) 是【非阻塞】的。 多路I/O转接 是由内核提供的 select、poll、epoll 监听机制。 监听connect 事件及 read、write事件。只有当 client 发生相应事件时,多路转接实现的服务器才会响应。 平时是非阻塞的(不会一直等待)。 select的实现如下图所示:
-
select
man 2 select 1.NAME select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing 2.SYNOPSIS /* According to POSIX.1-2001, POSIX.1-2008 */ #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); void FD_CLR(int fd, fd_set *set); //将一个文件描述符从集合中清除 int FD_ISSET(int fd, fd_set *set); //判断一个文件描述符是否在集合中 void FD_SET(int fd, fd_set *set); //将一个文件描述符添加到描述符集合中 void FD_ZERO(fd_set *set); //清空文件描述符集合 3.PARAMETER nfds :监听的所有文件描述符中,最大文件描述符+1。 //例:要监听 4~50的文件描述符(47), nfds就应该填 48 readfds :读 文件描述符监听集合。 传入 传出 参数。 传入:要设置读监听的文件描述符集合 传出:发生了读事件的文件描述符集合 //例:传入集合中有4、5、6文件描述符。而4文件描述符发生了读事件,故传出的集合中只存在文件描述符4。 writefds:写 文件描述符监听集合。 传入 传出参数 传入:要设置写监听的文件描述符集合 传出:发生了写时间的文件描述符集合 //例:传入集合中有4、5、6文件描述符。而5文件描述符发生了写事件,故传出的集合中只存在文件描述符5。 exceptfds:异常 文件描述符集合。 传入 传出 参数。 传入:要设置异常监听的文件描述符集合 传出:发生了异常事件 的文件描述符集合 //例:传入集合中有4、5、6文件描述符。而6文件描述符发生了异常事件,故传出的集合中只存在文件描述符6。 timeout: >0 :设置监听时长 NULL:阻塞监听(也就是始终等待事件发生) 0 :非阻塞监听,轮询 4.fd_set 是一个位图。 0 1表示。 5.RETURN VALUE >0 :所有监听集合(3个)中,所有传出集合的文件描述符的总数。 //例:以上面参数的例子为例,三个集合共传出了4、5、6故RETURN VALUE=3. =0 :没有满足监听条件的文件描述符 =-1 :ERROR. errno.
select服务器DEMO思路分析:
基础select demo实现链接
升级版select demo实现链接select 缺点 监听上限受文件描述符限制,最大 1024。检测满足条件的fd,需自己增加业务逻辑提高效率,也是变向增加了编码难度 优点 跨平台。可在 windowns、linux、macOS、Unix、类Unix、mips 上运行 -
poll
poll 就是升级版select (增加数组记录要遍历的文件描述符) man poll 1.NAME poll, ppoll - wait for some event on a file descriptor 2.SYNOPSIS #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); 3.PARAMETER fds :监听的文件描述符【数组】 struct pollfd { int fd; /* file descriptor */ //待监听的文件描述符 short events; /* requested events */ //待监听的文件描述符对应的监听事件 short revents; /* returned events */ //传入0。如果满足events给定的事件,会返回events对应的传入值。 }; events 或 revents 取值: POLLIN //读事件 POLLOUT //写事件 POLLERR //错误事件 nfds:监听数组的 实际有效监听个数。 timeout: >0 :超时时长。 单位:毫秒。 -1 :阻塞等待。 0 :不阻塞。 4.RETURN VALUE 满足对应监听事件的文件描述符 【总个数】。
poll 缺点 不能跨平台。只能在Linux或类Unix上运行。 不能直接定位到满足事件的文件描述符,也是变向增加了编码难度 优点 自带数组结构。可以将 监听事件集合 和 返回事件集合 分离。 可拓展 监听上线(方法同epoll)(超出1024限制)
十四、epoll
-
基础
epoll的本质是一个【红黑树】。监听结点为根节点。
epoll的使用由三个函数组成。 //epoll 应该使用非阻塞的ET模式写服务器程序(这是规则) man epoll_create man epoll_ctl man epoll_wait 1.epoll_create() //创建一棵监听红黑树 1)NAME epoll_create, epoll_create1 - open an epoll file descriptor 2)SYNOPSIS #include <sys/epoll.h> int epoll_create(int size); 3)PARAMETER size:创建红黑树的监听结点数量 (仅供内核初始化使用,当实际使用超出该大小时,内核会自动扩容) 4)RETURN VALUE On success, these system calls return a nonnegative file descriptor. On error, -1 is returned, and errno is set to indicate the error. 2.epoll_ctl() //操作监听红黑树 1)NAME epoll_ctl - control interface for an epoll file descriptor 2)SYNOPSIS #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 3)PARAMETER 1.epfd :epoll_create()函数的返回值 2.op :对该监听红黑树所做的操作。有三种值: EPOLL_CTL_ADD //添加 fd 到监听红黑树 EPOLL_CTL_MOD //修改 fd 在监听红黑树上的监听事件 EPOLL_CTL_DEL //将一个 fd 从监听红黑树上摘下(取消监听) 3.fd :待 op 操作的fd 4.event :本质为 struct epoll_event 结构体 指针。 struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; 成员 events 常用值: EPOLLIN EPOLLOUT EPOLLERR 成员 data 原型如下: typedef union epoll_data { void *ptr; int fd; //对应监听事件的fd uint32_t u32;//不用 uint64_t u64;//不用 } epoll_data_t; 4)RETURN VALUE When successful, epoll_ctl() returns zero. When an error occurs, epoll_ctl() returns -1 and errno is set appropriately. 3.epoll_wait() //阻塞监听红黑树 1)NAME epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor 2)SYNOPSIS #include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 3)PARAMETER 1.epfd :epoll_create()函数的返回值 2.events :传出参数(数组),满足定义的监听事件的所有结点的结构体数组 3.maxevents:events 数组元素的总个数。 默认为1024 。 struct epoll_event events[1024]; 4.timeout : -1 :阻塞 0 :不阻塞 -1 :失败 errno. 4)RETURN VALUE >0 :满足监听事件的struct epoll_event结构体的总个数。可用作循环处理的上线。 0 :没有满足监听事件的struct epoll_event结构体 -1 :失败 errno
epoll 缺点 优点 -
epoll 事件模型
ET 模式: (服务器常用方式) 即边沿触发。 缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。新的事件满足才会触发。 设置方式:struct epoll_event event; event.events = EPOLLIN | EPOLLET; 注意: ET模式只支持非阻塞。需给连接的文件描述符设置读写非阻塞(利用fcntl函数),如下: epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event); int flg = fcntl(cfd, F_GETFL); flg |= O_NONBLOCK; //位或操作 fcntl(cfd, F_SETFL, flg); LT 模式: 即水平触发。 ---epoll默认采用方式 缓冲区剩余未读尽的数据会导致 epoll_wait 返回。 例:通信时,readn 读数据时,每次只读 100字节,但缓冲区接收了 250 字节。 ① 默认的 LT模式下 会直接 再次触发 epoll_wait()函数,表明该连接fd又有数据写过来,并再读出100字节, 之后再次触发 epoll_wait()函数,表明又有数据写过来,接收50字节后会阻塞在readn的地方,直到写满100字节。 一旦 阻塞在readn的地方,代码就会有问题,因为应该阻塞在epoll_wait()的地方。 ② 在 ET模式下,剩下的150字节会被读取忽略,不会触发 epoll_wait()函数,而下次在写入数据时,会再从之前的150字节里读前100字节,后面的新写入的数据累加进来,等待下次在读取100字节。
【总结:应使用非阻塞的 ET模式写epoll服务器。】
-
epoll 反应堆
就是epoll基础demo的升级版,更完整些。 也是libevent 框架采用的方式。 epoll反应堆: ET模式 + 非阻塞、轮询 + void *ptr. 简单的 epoll demo: socket、bind、listen -- epoll_create 创建监听红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- -- while(1) -- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回监听满足数组。 -- 判断返回数组元素 -- -- lfd满足 -- Accept -- cfd满足 -- read -- 小--大 - 【diff content】 - write回去。 反应堆: 反应堆的优化就是在检测到对端有数据传输过来后,服务端写回时要检测,对端是否可写,确认可写再将数据写过去。 socket、bind、listen -- epoll_create 创建监听红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- -- while(1) -- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回监听满足数组。 -- 判断返回数组元素 -- -- lfd满足 -- Accept -- cfd满足 -- read -- 小--大 -- 【diff begin】 -- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 --epoll_ctl() -- EPOLL_CTL_ADD重新放到红黑树上监听 -- -- 等待 epoll_wait() 返回 -- 说明 cfd 可写 -- write写回去 -- cfd 从监听红黑树上摘下 -- EPOLLIN | EPOLLET -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听读事件 -- epoll_wait()监听 【diff end】