Linux网络编程
Linux C/C++网络编程
Socket编程
套接字概念
概念:
socket(套接字)
可以看成是用户进程与内核网络协议栈的编程接口socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信
socket(套接字)一定是成对出现的
一个套接字其实就是由一个文件描述符指向的(原理图中的sfd和cfd表示的就是文件描述符),因此socket通信中也可以使用I/O方法和函数
文件描述符
:- 是一个非负整数,并且用于代表一个打开的文件
- 文件描述符默认产生3个分别为0:STDIN_FILENO(标准输入)、1:STDOUT_FILENO(标准输出)、2:STDERR_FILENO(标准错误)
- 遵循新建文件描述符时选择最小可选择的,因此在没有新建文件描述符的打开一个文件产生的是fd为3的文件描述符
- 总共最多1024个文件描述符。也就是1023为最后一个文件描述符
字节序
概念:
字节序
是存储数据所遵循的一套规则- 字节序分为大端序和小端序
- 当我们传输数据时如果双方的字节序的不同就会造成发送的数据和接收数据不一致,所以我们得去关注字节序问题
- 字节序问题是一个历史遗留的问题,当年IBM也就是第一个数据存储公司,采用了大端序作为数据存储的规则,而Intel公司想要不一样导致了Intel使用了小端序的方法,因此导致了现在使用Intel、AMD等CPU的主机使用的是小端序存储,而网络协议族(TCP、IP等)使用大端序存储,导致网络通信上主机发送数据到网络上导致数据的不一致问题!
网络字节序
:规定为大端序,也就是网络中存储采用的字节序方法主机字节序
:根据CPU决定,一般为小端序,Intel、AMD采用的是小端序,表示主机存储采用的字节序方法- 当主机字节序和网络字节序不同时,要传输数据就需要进行字节序的转换
字节序:
大端序
:高位字节存在低位地址,低位字节存在高位地址例如:有一个十六位进制数0x12345678,利用大端序来存储
0x00000001(低位地址) 存 0x12(高位字节)
0x00000002 存 0x34
….
小端序
:高位字节存在高位地址,低位字节存在低位地址例如:有一个十六位进制数0x12345678,利用小端序来存储
0x00000001(低位地址) 存 0x78(低位字节)
0x00000002 存 0x56
….
字节序转换函数:
uint32_t htonl(uint32_t hostlong)
:将主机字节序转网络字节序(也就是小端序转大端序),用来进行转换IP地址的字节序(针对IP协议),无符号32位整型数通常对应着ip地址,对于一般的点分10进数的ip地址(例如192.168.1.110)是string类型需要用atoi()转换为无符号整型,所以不推荐使用这个转换函数1
2
3
4#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
hostlong->表示要转换的主机字节序形式的IPuint16_t htons(uint16_t hostshort)
:也是将主机字节序转换为网络字节序,但是用来进行转换端口号的字节序(针对端口号),无符号16位整型数通常对应端口号1
2
3
4#include <arpa/inet.h>
uint16_t htonl(uint16_t hostshort);
hostlong->表示要转换的主机字节序形式的端口uint32_t ntohl(uint32_t netlong)
:将网络字节序转换为主机字节序(也就是大端序转换为小端序),用来进行转化IP地址的字节序1
2
3
4#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);
netlong->表示要转换的网络字节序形式的IPuint16_t ntohs(uint16_t netshort)
:也是将网络字节序转换为主机字节序,但是用来进行转化端口号字节序的1
2
3
4#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);
netshort->表示要转换的网络字节序形式的端口号
注:
- 使用以上函数需要包含头文件
#include <arpa/inet.h>
- h表示host(主机),n表示network(网络),l表示32为长整数,s表示16位短整数
IP地址转换函数:
int inet_pton(int af,const char *src,void *dst)
:用于将点分十进制的IP地址转换为网络字节序的IP地址1
2
3
4
5
6
7
8
9
10
11
12
13#include <arpa/inet.h>
int inet_pton(int af,const char *src,void *dst);
//参数:
af:表示IP协议(ipv4,ipv6),填AF_INET(IPv4)或AF_INET6(IPv6);
src:传入,IP地址(点分十进制);
dst:传出,转换后的网络字节序的IP地址;
//返回值:
1:转换成功;
0:转换异常,说明src指向的不是一个有效的IP地址;
-1:转换失败;const char *inet_ntop(int af,const void *src,char *dst,socklen_t size)
:用于将网络字节序的IP地址转换为主机字节序的点分十进制IP地址(也就是string类型的)1
2
3
4
5
6
7
8
9
10
11
12
13#include <arpa/inet.h>
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
//参数:
af:表示IP协议(ipv4,ipv6),填AF_INET(IPv4)或AF_INET6(IPv6);
src:网络字节序IP地址;
dst:传出,主机字节序IP地址(string);
size:dst的大小;
//返回值:
dst:转换成功返回;
NULL:转换失败返回;struct hostent *gethostbyname(const char *name)
:用于将域名、主机名、字符串IP转换为网络字节序IP地址,用于客户端程序1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24#include <netdb.h>
struct hostent *gethostbyname(const char *name);
name->IP地址
//返回一个hostent结构体
struct hostent{
char *h_name; //主机名
char **h_aliases; //主机所有别名构成的字符串数组,同一IP可绑定多个域名
short h_addrtype; //主机IP地址的类型
short h_length; //主机IP地址长度,IPv4为4,IPv6为6
char **h_addr_list; //主机的IP地址,以网络字节序存储
};
//返回值
nullptr:转换失败;
hostent结构体:转换成功
//客户端通信使用
#define h_addr h_addr_list[0]
struct sockaddr_in s;
//将转换后的网络字节序IP地址复制到sockaddr_in结构体的sin_addr成员中
memcpy(&s.sin_addr,h->h_addr,h->h_length);in_addr_t inet_addr(const char *cp)
:将字符串格式也就是点分十进制的IP地址转换为网络字节序IP地址char *inet_ntoa(struct in_addr in)
:将网络字节序的IP地址转换为字符串格式IP地址,用于服务端解析客户段IP地址
sockaddr数据结构
概念:
- 在实现网络通信中的接口时,我们常常得使用他们给定的特定结构体作为参数进行传入,而sockaddr结构体就是最早的结构体
网络结构体:
sockaddr结构体
:用于存放协议族、端口和地址信息的结构体,客户端和服务端中的接口函数需要使用这个结构体,但是这个结构体将端口和地址信息都用同一个变量进行存储1
2
3
4struct sockaddr{
sa_family_t sin_family; //协议族,一般填AF_INET,IPv6填AF_INET6
char sa_daat[14]; //14字节,包含目标地址和端口号
};sockaddr_in结构体
:现在一般都使用这个结构体,他和sockaddr结构体的大小一样,只是将目标IP地址和端口号单独分出来,但是网络通信的接口都是用sockaddr作为参数,因此需要强转为sockaddr结构体1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35#include <netinet/in.h>
struct sockaddr_in{
sa_family_t sin_family; //协议族,一般填AF_INET,IPv6填AF_INET6
in_port_t sin_port; //网络字节序的端口号
struct in_addr sin_addr; //网络字节序的IP地址
char sin_zero[8]; //未使用,为了保持与sockaddr一样的长度所添加的
};
struct in_addr{
uint32_t s_addr; //网络字节序的IP地址,32位地址
};
//初始化
struct sockaddr_in s;
s.sin_family=AF_INET or AF_INET6;
s.sin_port=htons(port); //将主机字节序端口转换为网络字节序再赋值
//如果IP地址是整形的主机字节序IP地址
s.sin_addr.s_addr=htonl(IP);
//如果IP地址是整形的点分十进制IP地址
int dst;
inet_pton(AF_INET,IP,(void *)&dst);
s.sin_addr.s_addr=dst;
或
s.sin_addr.s_addr=inet_addr(IP);
>>重点
/*【重点】*/
//无论IP地址啥格式直接使用下面这段代替
//使用宏INADDR_ANY,会自动取出系统中有效的任意IP地址,是主机字节序形式的
s.sin_addr.s_addr=htonl(INADDR_ANY); //将主机字节序转换为网络字节序
//强转sockaddr
(struct sockaddr *)&s;sockaddr_in6
:IPv6的sockaddr_in结构体sockaddr_un
:本地套接字所用
网络套接字函数
socket模型创建流程
服务端流程:
- socket()创建一个套接字s1
- 使用bind()函数绑定IP地址和port端口号
- 使用listen()设置同时与服务器建立连接的上限
- accept()阻塞监听客户端连接,如果连接成功,释放s1,让s1继续监听客户端连接,返回一个新的套接字s2
- 使用read()、recv()读取客户端发送过来的请求与数据
- 使用write()、send()回应客户端请求与数据
- 当read()函数读到0时终止与客户端的连接
- 使用close()关闭服务端连接
客户端流程:
- socket()创建套接字s3
- 使用connect()将套接字s3绑定IP地址和port端口,并与客户端连接
- 使用write()、send()向服务端发送请求和数据
- 使用read()、recv()接收服务端的响应和回应
- 使用close()关闭连接
socket模型创建总体流程:
注:
- 总体流程一共创建了3个socket套接字
- read和write函数是对文件IO的API,send和recv是socket编程的API,因为socket实质也是一个文件描述符,所以也可以使用read和write进行发送以及接收数据
函数和方法
概念:网络通信所需要使用的函数和接口
函数和方法
int socket(int domain,int type,int protocol)
:创建一个套接字
1 |
|
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
:给socket套接字绑定一个地址结构(IP地址+端口号)
1 |
|
int listen(int sockfd,int backlog)
:设置同时与服务器建立连接的上限数(同时进行3次握手的客户端数量)
1 |
|
- 【重点】
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
:*阻塞等待客户端建立连接,成功返回一个与客户端连接的sockfd(socket文件描述符)
1 |
|
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
:客户端使用函数,使用现有的socket与服务器建立连接,可以不需要使用bind函数绑定客户端地址结构,因为他会采用”隐式绑定”地址结构
1 |
|
ssize_t read(int fd,void *buf,size_t count)
:原本是用于文件读取的操作,但是也可以用与socket的接收以及读取数据的操作
1 |
|
ssize_t write(int fd,const void *buf,size_t count)
:用于进行文件写数据的操作,但是也可以用于socket通信的发送以及写数据的操作
1 |
|
ssize_t send(int sockfd,const void *buf,size_t len,int flags)
:用于网络套接字上发送数据和请求,只能用于TCP
1 |
|
ssize_t recv(int sockfd,void *buf,size_t len,int flags)
:用于在网络套接字接收数据或请求,只能用于TCP
1 |
|
int shutdown(int sockfd,int how)
:跟close函数差不多的功能,但是shutdown不考虑描述符的引用计数,直接关闭描述符,并且可以选择关闭读端还是写端还是读写端
1 |
|
注:
- 使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接.
- shutdown不考虑描述符的引用计数,直接关闭描述符.
- 例如当我们使用dup2进行文件描述符重定向时,使用close源文件描述符并不能阻止其他指向源文件的文件描述符操作,而shutdown会关闭全部.
C/S模型 -TCP
server的实现
1 |
|
client的实现
1 |
|
TCP状态时序图
TCP状态转换图:
TCP各阶段状态图:
出错处理函数封装
函数声明
wrap.h
1 |
|
具体函数封装
wrap.cpp
1 |
|
新client
1 |
|
新server
1 |
|
端口复用
概念:
端口复用
:解决服务端先关闭导致服务端并未真正关闭,导致端口被占用需要等待2MSL(40s左右)才能重新启动同一端口的服务端程序问题,使端口复用!端口复用解决的具体问题产生原因:
当我们每次先关闭服务端程序运行再关闭客户端的时候,我们会发现再次启动服务端程序会发现报错说端口正在被使用,这涉及到了TCP中的连接问题,在TCP的四次挥手中,TCP会提供客户端未收到服务端的FIN包的等待重传机制(这个时候服务端并未真正处于CLOSED状态),将会导致客户端不知道服务端是否收到导致ack包到底发不发的问题,因此TCP设置让客户端等待2MSL(40多s)后再继续重发机制!而这个2MSL中服务端是处于TIME_WAIT状态,并未真正关闭,因此我们需要等待2MSL时间才能成功启动服务端程序
函数原型:
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen)
:套接字设置函数,可以设置套接字的选项以及属性(选项很多复杂),我们这里用来进行端口复用,这里只讨论端口复用如何使用
1 |
|
注:需要在socket函数和bind函数之间进行执行!
多进程并发服务器
实现思路:
- 创建监听套接字sfd,Socket()
- 绑定地址结构sockaddr_in并强转成sockaddr,Bind()
- 设置监听上限,Listen()
- 父进程(pid>0)进行监听,创建cfd与子进程(pid==0),注册信号捕捉函数非阻塞回收子进程,父进程关闭通信套接字cfd,Accept(),fork(),close(cfd),SIGCHLD,waitpid()。
- 子进程接收客户端连接请求以及执行服务端代码,子进程关闭监听套接字sfd,close(sfd),read()
实现示例:
1 |
|
可以使用
scp -r 本地代码目录 目标服务器用户名@公网IP地址:要存入的路径
,将代码上传到服务器上测试
多线程并发服务器
实现思路:
- 创建监听套接字sfd,Socket()
- 绑定地址结构sockaddr_in并强转成sockaddr,Bind()
- 设置监听上限,Listen()
- 父线程进行监听,创建子线程和cfd,使用线程分离非阻塞回收子线程(如果需要返回值,则需要创建一个专门子线程回收子线程,pthread_join回收子线程退出值),父线程关闭通信套接字cfd,Accept(),pthread_create(),pthread_detach,close(cfd),
- 子线程接收客户端请求并执行服务端代码,子线程关闭监听套接字sfd,close(sfd),read()
实现示例:
1 |
|
多路I/O转接服务器
引言
- 之前讲的服务端程序使用的方式就是阻塞模式或者非阻塞忙轮询模式,而这些模式的效率很低,因此我们引入了响应式的多路IO转接模式
阻塞
:一直等待事件发生非阻塞忙轮询
:就是不阻塞,但是会一直循环查看事件是否发生多路I/O转接
:响应式,当事件发生就唤醒服务端- 多路IO转接服务器也叫做多任务IO服务器,这类服务器实现的主旨思想是,不再游应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件
select模型
概念:
select模型
:采用内核作为代理代替应用程序进行轮询,解决了accept监听阻塞等待问题,并没有解决客户端与服务端通信之间的阻塞- 在非阻塞忙轮询的模式中,我们的应用程序需要不断地轮询(polling)获得客户端的请求,而每次轮询就是一次系统调用,这样的效率不够好,而在select模型中,我们选择使用内核作为代理代替应用程序去轮询
select模型原理:
- select作为内核代理,将会持有原先服务端的监听套接字sfd,来监听客户端,解放服务端程序
- 对于多个客户端连接套接字cfd(文件描述符),select会将其放入一个等待队列中
- 然后select调用的时候会遍历队列,如果客户端发送请求或数据,会调用服务端程序的回调事件(也就是服务端和客户端还是直接通信,select并未进行协助)
- 当遍历结束之后,如果仍然没有一个可用的文件描述符(也就是客户端没有请求),select会让用户进程睡眠,直到等待资源可用的时候(也就是客户端有请求时)再唤醒用户进程并返回对应的文件描述符(调用应用进程的回调事件)
优点:
- 跨平台进行文件描述符监听,例如windows、macos、linux、Unix、类Unix、mipe系统都可以使用
缺点:
- 监听上限受文件描述符限制,最大1024个
- select模型需要遍历所有文件描述符队列,没有监听或者事件发生的文件描述符也被遍历,导致效率不够理想
- select的每次调用都需要重复的复制集合,而且在select内部需要将集合从用户区复制到内核区,等有事件来又需要将数据从内核区复制到用户区,导致select的性能受限
函数:
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout)
:创建select
1 |
|
void FD_ZERO(fd_set *set)
:把文件描述符集合(监听集合)清空
1 |
|
void FD_CLR(int fd,fd_set *set)
:把文件描述符集合某个文件描述符从监听集合中移除
1 |
|
void FD_SET(int fd,fd_set *set)
:将fd文件描述符添加到set监听集合中
1 |
|
int FD_ISSET(int fd,fd_set *set)
:判断fd文件描述符是否存在于set监听集合中
1 |
|
select实现多路IO转接服务器:
1 |
|
优化:
- 对于select模型需要遍历不必遍历的文件描述符而造成的效率丢失,我们可以使用一个自定义数组用于存储客户端的文件描述符来提高效率
1 |
|
poll模型
概念:
- poll本质上是对select的改进,但是由于一些原因,poll并没有解决性能问题,只是在参数层面做了优化和解除了通过宏定义来设置的限制。因此poll只能说是一个半成品
函数原型:
int poll(struct pollfd *fds,nfds_t nfds,int timeout)
:创建poll
1 |
|
优点:
- 自带数组结构
- 可以将监听事件集合和返回事件集合分离,无需每次调用时,重新设定监听事件
可以拓展监听上限
,超出1024上限
缺点:
- 不能跨平台,只能在Linux系统下使用
- 无法直接定位满足监听事件的文件描述符,需要挨个轮询
poll实现多路IO转接服务器:
1 |
|
epoll模型
概念:
epoll
复用了等待队列,并且添加了就绪列表,当监听事件发生时,进行回传通知,而事件通知的时机又提供了水平触发(LT)和边缘触发(ET)epoll既提供了select/poll的IO事件的水平触发(LT),也提供了边缘触发(ET)机制,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的使用,提供应用程序效率
等待队列
:采用红黑树进行存储被监听的客户端文件描述符就绪列表
:使用双向链表进行存储事件触发就绪的文件描述符epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率
原因:
- 因为它会复用文件描述符集合(等待队列)来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合
- 无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的文件描述符集合即可
ET和LT模式:
边缘触发(ET)
:全名edge trigger,只有新事件触发,或是timeout时epoll_wait才会回传,不管缓冲区是否还有数据,只支持非阻塞socket
,使用fcntl函数设置非阻塞套接字优点:开销小,不用过度处理消息
缺点:编码难度大,需要注意事件无法一次性处理完,导致下次触发事件却处理着上次事件剩下的数据(遗漏事件处理)
水平触发(LT)
:默认采用模式,全名level trigger,epoll_wait在事件状态未变更前都会回传,只要缓冲区有数据都会触发优点:编码难度较简单,不会遗漏事件处理
缺点:开销较大
注意:
- epoll的本质就是监听文件描述符,因此epoll不仅可以用来监听套接字,也可用于管道、fifo、mmap映射等
突破1024文件描述符限制:
- 可以使用cat命令查看当前计算机可以打开的socket描述符上限,受硬件影响
1 |
|
- 使用ulimit命令查看当前用户下的进程,默认打开文件描述符上限,缺省为1024
1 |
|
- 修改配置文件修改上限值
1 |
|
工作流程:
- epoll维护一个等待队列(红黑树)和一个就绪列表(链表)
- 然后需要监听的socket文件描述符就添加到等待队列中
- 当哪个客户端socket有事件触发,就将其加入到就绪列表中
- 然后回传通知给服务端,服务端直接遍历就绪列表中,挨个处理即可
函数原型:
int epoll_create(int size)
:创建epoll文件描述符,也就是指向红黑树根节点的文件描述符
1 |
|
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
:用来控制红黑树
1 |
|
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)
:监听等待队列的客户端socket是否有监听事件触发
1 |
|
epoll实现多路IO转接服务器:
LT模式
:
1 |
|
非阻塞ET模式
1 |
|
非阻塞ET注意问题:
掉连接问题
:当我们的监听套接字设定为非阻塞时,多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理,这样就是我们所说的掉连接
解决方法
:使用while循环抱住accept调用,使得一次accept处理完就绪队列中的就绪连接再结束,或者在设置监听套接字阻塞以及事件上使用LT模式
事件丢失/未处理完问题
:使用非阻塞模式的ET模式,容易因为网络问题、数据断传或者服务端读取字节小于数据,导致一部分数据得等到下个新事件就绪才能获取到,这样就会导致最后一个事件并没有被真正处理,导致事件丢弃/未处理完问题
解决办法
:使用while循环抱住read函数,使得缓冲区可读就一直读下去,使得一次读取干净,相对应的写事件也是一样的处理方式
Reactor模型
概念:
Reactor模型
:又称epoll反应堆模型,是基于epoll的ET模式实现的,这个模型即为非阻塞IO+IO复用的同步IO模型操作流程
:通过IO多路复用检测多条连接的IO是否就绪,如果有IO就绪,再通过非阻塞IO去操作具体IOReactor模式是一个事件驱动机制模型
,他逆转了事件处理的流程,不再是检测发生事件类型来调用对应api处理,而是应用程序在Reactor上提前注册好回调函数(使用泛型指针ptr来注册),如果相应事件发送,reactor就直接调用应用程序注册的接口注意
:Reactor模型还分几种,我们这里先讲述单reactor单线程模型,所以以下内容都只是单reactor单线程模型的内容,但是基本思想一致
Reactor模型弃用epoll_data中的fd设置fd,而是使用成员ptr
- Reactor模型不再使用epoll_data(联合体)中的fd来设置fd,而是可以使用泛型指针ptr来进行设定
- 因为ptr是泛型指针,并且epoll_data是联合体,里面成员共用一个地址空间,所以我们可以自定义结构体里面包含fd和回调函数等等成员,这样我们就可以通过一个ptr操作多个成员以及使用结构体
reactor架构方案如下图:
区别:
- select、poll、epoll只解决了IO是否就绪问题,也就是IO多路复用,并没有解决具体IO操作问题
- 而reactor使用非阻塞IO+IO多路复用,就既解决了IO是否就绪问题,也解决了具体IO操作问题
工作流程:
- 配置服务端配置,socket、bind、listen,设置非阻塞后
- 使用epoll_create创建监听红黑树
- epoll_ctl添加监听套接字lfd
- 循环epoll_wait监听,有事件发生,返回触发事件数组
- 当监听套接字lfd有事件发生,创建新客户端cfd,并添加到红黑树上
- 当cfd有读事件触发,执行read(),将cfd从监听红黑树摘下
- 重新添加一个写事件就绪的cfd(刚刚摘下的cfd)到监听红黑树上,需要设定事件为写事件(EPOLLOUT),以及注册好回调函数(使用ptr)
- 等待epoll_wait返回,也就是cfd可写,然后将数据write回去
- 将cfd从红黑树摘下,将事件改为读事件EPOLLIN,将其重新放回红黑树,然后继续监听
- 剩下就是重复之前步骤
代码结构:
myevent_s结构体
:替换掉event_data结构体中的ptr,用来设置事件参数包括文件描述符,监听事件类型以及回调函数等等.1
2
3
4
5
6
7
8
9
10
11//描述就绪文件描述符相关信息
struct myevent_s{
int fd; //要监听的文件描述符
int events; //对应的监听事件
void *arg; //泛型参数
void (*call_back)(int fd,int events,void *arg); //回调函数
int status; //判断是否监听参数,1:监听,0:不在监听
char buf[BUFLEN]; //缓冲区
int len; //缓冲区长度
long last_active; //记录每次加入红黑树g_efd的时间,用于超时设定
};initlistensocket函数
:创建和初始化监听套接字lfd1
2
3
4
5void initlistensocket(int efd,short port);
efd:epoll文件描述符,指向监听红黑树的根节点;
port:端口号,绑定监听套接字lfd上的服务端端口;eventset函数
:初始化事件结构体myevent_s,设置回调函数1
2
3
4
5void eventset(struct myevent_s *ev,int fd,void(*call_back)(int,int,void*),void *arg);
ev:就绪文件结构体,用来描述就绪文件描述符的信息结构体,自定义结构体,事件参数结构体event_data的ptr指向的结构体;
fd:要监听的文件描述符;eventadd函数
:将fd以及对应事件结构体添加到监听红黑树,设置监听事件。1
2
3
4
5
6
7void eventadd(int efd,int events,struct myevent_s *ev);
efd:epoll文件描述符,指向监听红黑树的根节点;
events:设置对应监听事件的参数;
ev:就绪文件结构体,用来描述就绪文件描述符的信息结构体,自定义结构体,事件参数结构体event_data的ptr指向的结构体;eventdel函数
:从epoll监听的红黑树删除文件描述符1
2
3
4
5void eventdel(int efd,struct myevent_s *ev);
efd:epoll文件描述符;
ev:要删除的文件描述符的就绪文件结构体(里面包含了要删除的文件描述符);acceptconn函数
:监听套接字lfd的回调函数,用于服务端创建与客户端的连接1
2
3
4
5
6
7void acceptconn(int lfd,int events,void *arg);
fd:lfd;
eventds:要监听的事件类型;
arg:用来传入lfd对应的就绪结构体myevent_s参数recvdata函数
:客户端套接字cfd的读就绪事件的回调函数,服务端接收客户端请求和数据1
2
3
4
5
6
7void recvdata(int fd,int events,void *arg);
fd:cfd;
events:监听事件(这个地方是读事件);
arg:用来传入cfd对应的就绪结构体myevent_s参数;senddata函数
:客户端套接字cfd的写就绪事件的回调函数,服务端的向客户端发送数据1
2
3
4
5
6
7void senddata(int fd,int events,void *arg);
fd:cfd;
events:监听事件(这个地方是写事件);
arg:用来传入cfd对应的就绪文件结构体myevent_s参数;
单reactor单线程模型示例代码:
1 |
|
线程池
引言与分析:
- 在多进程、多线程并发以及多路io转接服务器中,我们说多路IO转接服务器的效率较高,是因为多进程、多线程服务器会为客户端的到来而创建进程和线程,然后为客户端的离开将进程、线程销毁回收掉,而频繁这样操作将会大大消耗系统资源,而多路IO复用不需要,它只有一个进程,很多事情也交给了内核处理
- 对于以上问题,我们可以避免多次的创建进程和线程,我们可以在程序开始时就一次性创建多个线程或进程来处理。
- 这样就引出了以下的线程池
概念:
线程池
:顾名思义就是一个放线程的池子(线程聚集的地方),里面有很多线程,用于管理程序的线程创建与销毁回收的,会在程序开始时创建一堆线程。- 对于多线程并发服务器中,我们使用的是类似于生产者消费者模型,线程池的作用就是用于处理客户端发送的数据,当有一个客户端发送数据来时,线程池就会为此分配一个线程来处理
实现前提分析:
初始线程数
:当我们要为服务端起始创建线程池,我们需要对具体服务功能分析出初始线程数,并设定初始线程池中线程数线程池扩容机制
:为了解决高峰期线程不够以及不及时补给问题,我们需要设置线程池的扩容机制,我们可以根据线程池空闲线程live_num跟正在运行的线程数busy_num的比例来进行扩容,例如当我的线程池为38时,当busy_num/live_num大于等于80%时就进行扩容指定个数的线程线程池最大线程数
:有了扩容机制,我们也不能无限的扩容,所以得设置线程池最大线程数,防止扩容越界线程池瘦身机制
:有了扩容机制相对应的也得有瘦身机制,当高峰期过去时,我们就不需要这么多的线程来处理工作了,就得进行减少线程池中的线程数,因此我依然可以根据线程池空闲线程live_num跟正在运行的线程数busy_num的比例来进行瘦身,例如当busy_num/live_numx小于等于20%时就进行减少指定个数的线程管理线程
:对于以上扩容、瘦身操作,我们也不应该让服务器进行处理增加服务器压力,因此我们需要创建一个管理线程来专门进行管理线程池,让他进行扩容、瘦身和销毁等操作锁
:我们使用线程池的线程进行处理数据,对于任务队列进行读写处理等操作,为了保证数据的正确性,所以我们使用锁来进行线程同步条件变量
:对于生产者消费者模型,我们线程应该当且仅当任务队列有任务才进行竞争处理,在没有数据时,我们不应该使用阻塞的方式去等待,因此我们需要使用条件变量来进行非阻塞等待,当任务队列中有任务要处理再将线程唤醒
具体实现:
- 程序运行开始时,先创建线程池并初始化线程池结构体成员变量,为线程池结构体部分成员分配空间,锁、条件变量的初始化以及为处理任务线程、管理线程绑定执行主体函数
- 当线程池初始化后,会创建最小线程池大小的线程数,然后阻塞等待条件变量(队列不为空时,就会有任务来的条件变量)的唤醒
- 任务到来,如果任务队列满了,就将会阻塞等待队列不为满的条件变量的唤醒,否则将其添加到线程池中的任务队列中,然后将阻塞在队列不为空的条件变量上的线程唤醒
抢到任务线程
从任务队列中取出任务后(任务队列出队),然后就可以通知阻塞等待队列不为满的线程(如果任务队列满了的时候,没满不会影响),告诉他们可以添加任务了,然后抢到任务的线程将执行任务- 任务处理结束后,则线程自行退出
管理线程
将会循环检查线程池中的忙线程数、存活线程数(空闲线程数)、最大线程数、最小线程数和任务队列任务数之间的关系,判断是否执行扩容和瘦身- 判断扩容算法,任务数大于最小线程池个数,且存活的线程数少于最大线程个数时,如:30>=10&&40<100
- 判断瘦身算法,忙线程*2小于存活的线程数,并且存活的线程数大于最小线程数时
- 当程序结束运行前,将线程池销毁,并将分配的空间资源进行释放
注意:
- 在
单线程池
中,内核是不会自动帮忙处理任务的回调函数,因此得手动在处理任务线程的执行主体函数手动添加处理
示例代码:
结构体定义
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//各子线程任务结构体
struct threadpool_task_t {
void *(*function)(void *); //回调函数
void *arg; //回调函数参数
};
//描述线程池相关信息
struct threadpool_t {
pthread_mutex_t lock; //用于锁住本结构体
pthread_mutex_t thread_cnt; //记录忙状态线程个数的锁 ---busy_thr_num
pthread_cond_t quque_not_full; //当任务队列满时,添加任务的线程阻塞,等待此条件变量
pthread_cond_t quque_not_empty; //任务队列不为空时,通知等待任务的线程
pthread_t *threads; //存放线程池中每个线程的tid,数组
pthread_t adjust_tid; //管理线程tid
threadpool_task_t *task_queue; //任务队列(数组首地址)
int min_thr_num; //线程池最小线程数
int max_thr_num; //线程池最大线程数
int live_thr_num; //当前存活线程个数
int busy_thr_num; //忙状态线程个数
int wait_exit_thr_num; //要销毁的线程个数
int queue_front; //task_queue队头下标
int queue_rear; //task_queue队尾下标
int queue_size; //task_queue队中实际任务数
int queue_max_size; //task_queue队列可容纳任务数上限
int shutdown; //标志位,线程池使用状态,true或false
};函数定义
:1
2
3
4
5
6
7
8
9
10
11
12
13
14//创建线程池
threadpool_t *threadpool_create(int min_thr_num,int max_thr_num,int queue_max_size);
//向线程池添加任务,借助回调函数处理数据
int threadpool_add(threadpool_t *pool,void*(*function)(void *arg),void *arg);
//销毁线程池
int threadpool_destroy(threadpool_t *pool);
void *threadpool_thread(void *threadpool);
void *adjust_thread(void *threadpool);
// int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);具体代码
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#define DEFAULT_TIME 10
#define DEFAULT_THREAD_VARY 10
#define true 1
#define false 0
threadpool_t *threadpool_create(int min_thr_num,int max_thr_num,int queue_max_size){
int i;
//线程池结构体
threadpool_t *pool=NULL;
do{
if((pool=(threadpool_t *)malloc(sizeof(threadpool_t)))==NULL){
printf("malloc threadpool fail\n");
break;
}
pool->min_thr_num=min_thr_num;suan f
pool->max_thr_num=max_thr_num;
pool->busy_thr_num=0;
pool->live_thr_num=min_thr_num; //活着的线程数初值=最小线程数
pool->wait_exit_thr_num=0;
pool->queue_size=0;
pool->queue_max_size=queue_max_size;
pool->queue_front=0;
pool->queue_rear=0;
pool->shutdown=false;
//根据最大线程数上限数,给工作线程数组开辟空间,并清零
pool->threads=(pthread_t *)malloc(sizeof(pthread_t)*max_thr_num);
if(pool->threads==NULL){
printf("malloc threads fail\n");
break;
}
memset(pool->threads, 0, sizeof(pthread_t)*max_thr_num);
suan f
//给任务队列开辟空间
pool->task_queue=(threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
if(pool->task_queue==NULL){
printf("malloc task_queue fail\n");
break;
}
if(pthread_mutex_init(&(pool->lock),NULL)!=0
|| pthread_mutex_init(&pool->thread_cnt,NULL)!=0
|| pthread_cond_init(&pool->quque_not_empty,NULL)!=0
|| pthread_cond_init(&(pool->quque_not_full), NULL)!=0){
printf("init the lock or cond fail\n");
break;
}
for(i=0;i<min_thr_num;++i){
//pool指向当前线程池
pthread_create(&(pool->threads[i]),NULL,threadpool_thread,(void *)pool);
printf("start thread 0x%x...\n",(unsigned int)pool->threads[i]);
}
//创建管理者线程
pthread_create(&(pool->adjust_tid),NULL,adjust_thread,(void *)pool);
return pool;
}while(0);
//前面代码调用失败时,释放pool存储空间
threadpool_free(pool);
return NULL;
}
//向线程池添加一个任务
int threadpool_add(threadpool_t *pool,void*(*function)(void *arg),void *arg){
pthread_mutex_lock(&pool->lock);
//==为真,队列已经满了,调wait阻塞
while ((pool->queue_size==pool->queue_max_size)&&(!pool->shutdown)) {
pthread_cond_wait(&pool->quque_not_full,&pool->lock);
}
if(pool->shutdown){
pthread_cond_broadcast(&pool->quque_not_empty);
pthread_mutex_unlock(&pool->lock);
return 0;
}
//清空工作线程调用的回调函数的参数arg
if(pool->task_queue[pool->queue_rear].arg!=NULL){
pool->task_queue[pool->queue_rear].arg=NULL;
}
//添加任务到任务队列中
pool->task_queue[pool->queue_rear].function=function;
pool->task_queue[pool->queue_rear].arg=arg;
pool->queue_rear=(pool->queue_rear+1)%pool->queue4_max_size; //队尾指针移动,模拟环形
pool->queue_size++; //任务队列中实际任务数增加
//添加完任务后,队列不为空,唤醒线程池中等待处理任务诉的线程
pthread_cond_signal(&pool->quque_not_empty);
pthread_mutex_unlock(&pool->lock);
return 0;
}
void *threadpool_thread(void *threadpool){
threadpool_t *pool=(threadpool_t*) threadpool;
threadpool_task_t task;
while (true) {
//刚创建出线程,等待任务队列出现任务,否则阻塞等待任务队列里有任务后再唤醒接收任务
pthread_mutex_lock(&pool->lock);
//queue_size==0 说明没有任务,调wait阻塞在条件变量上,若有任务,跳过该while
while ((pool->queue_size==0)&&(!pool->shutdown)) {
printf("thread 0x%x is waiting\n",(unsigned int)pthread_self());
pthread_cond_wait(&(pool->quque_not_empty),&(pool->lock));
//被唤醒说明有任务需要分配线程处理,空闲线程减少
if(pool->wait_exit_thr_num>0){
pool->wait_exit_thr_num--;
//如果线程池线程个数大于最小值时可以结束当前线程
if(pool->live_thr_num>pool->min_thr_num){
printf("thread 0x%x is exiting\n",(unsigned int)pthread_self());
pool->live_thr_num--;
pthread_mutex_unlock(&pool->lock);
pthread_exit(NULL);
}
}
}
//如果指定了true,表示线程池关闭,则需要将线程池里的每个线程关闭,自行退出处理--销毁线程池
if(pool->shutdown){
pthread_mutex_unlock(&pool->lock);
printf("thread 0x%x is exiting\n",(unsigned int)pthread_self());
pthread_detach(pthread_self());
pthread_exit(NULL); //线程自行结束
}
task.function=pool->task_queue[pool->queue_front].function;
task.arg=pool->task_queue[pool->queue_front].arg;
//出队,模拟环形队列
pool->queue_front=(pool->queue_front+1)%pool->queue_max_size;
pool->queue_size--;
//通知可以有新的任务添加进来
pthread_cond_broadcast(&pool->quque_not_full);
//任务取出后,立即将线程池锁释放
pthread_mutex_unlock(&pool->lock);
//执行任务
printf("thread 0x%x start working\n",(unsigned int)pthread_self());
pthread_mutex_lock(&pool->thread_cnt); //忙线程数变量锁
pool->busy_thr_num++; //忙线程数+1
pthread_mutex_unlock(&pool->thread_cnt);
//模拟处理任务
task.function(task.arg); //执行回调函数任务
// (*task.function)(task.arg);
//任务结束处理
printf("thread 0x%x end working\n",(unsigned int)pthread_self());
pthread_mutex_lock(&pool->thread_cnt);
pool->busy_thr_num--; //处理掉一个任务,忙线程数--
pthread_mutex_unlock(&pool->thread_cnt);
}
pthread_exit(NULL);
return NULL;
}
//管理线程
void *adjust_thread(void *threadpool){
int i;
threadpool_t *pool=(threadpool_t *)threadpool;
while (!pool->shutdown) {
//定时对线程池管理
sleep(DEFAULT_TIME);
pthread_mutex_lock(&pool->lock);
int queue_size=pool->queue_size; //任务数
int live_thr_num=pool->live_thr_num; //存活线程数
pthread_mutex_unlock(&pool->lock);
pthread_mutex_lock(&pool->thread_cnt);
int busy_thr_num=pool->busy_thr_num; //忙线程数量
pthread_mutex_unlock(&pool->thread_cnt);
//创建新线程算法:任务数大于最小线程池个数,且存活的线程数少于最大线程个数时,如:30>=10&&40<100
if(queue_size>live_thr_num&&live_thr_num<pool->max_thr_num){
pthread_mutex_lock(&pool->lock);
int add=0;
//一次增加DEFAULT_THREAD个线程
for(i=0;i<pool->max_thr_num&&add<DEFAULT_THREAD_VARY
&&pool->live_thr_num<pool->max_thr_num;++i){
if(pool->threads[i]==0){
pthread_create(&pool->threads[i],NULL,threadpool_thread,(void *)pool);
add++;
pool->live_thr_num++;
}
}
pthread_mutex_unlock(&pool->lock);
}
//销毁多余的空闲线程,算法:忙线程*2小于存活的线程数,并且存活的线程数大于最小线程数时
if((busy_thr_num*2)<live_thr_num&&live_thr_num>pool->min_thr_num){
//一次销毁DEFAULT_THREAD个线程,随机10个即可
pthread_mutex_lock(&pool->lock);
pool->wait_exit_thr_num=DEFAULT_THREAD_VARY; //要销毁的线程数设置为10
pthread_mutex_unlock(&pool->lock);
for(i=0;i<DEFAULT_THREAD_VARY;++i){
//通知处在空闲状态的线程,他们会自行终止
pthread_cond_signal(&pool->quque_not_empty);
}
}
}
return NULL;
}
//销毁线程池
int threadpool_destroy(threadpool_t *pool){
int i;
if(pool==NULL){
return -1;
}
pool->shutdown=true;
//先销毁管理线程
pthread_join(pool->adjust_tid,NULL);
for(i=0;i<pool->live_thr_num;++i){
//通知所有空闲线程
pthread_cond_broadcast(&pool->quque_not_empty);
}
for(i=0;i<pool->live_thr_num;++i){
pthread_join(pool->threads[i],NULL);
}
threadpool_free(pool);
return 0;
}
//释放线程池空间
int threadpool_free(threadpool_t *pool){
if(pool==NULL){
return -1;
}
if(pool->task_queue){
free(pool->task_queue);
}
if(pool->threads){
free(pool->threads);
pthread_mutex_lock(&pool->lock);
pthread_mutex_destroy(&pool->lock);
pthread_mutex_lock(&pool->thread_cnt);
pthread_mutex_destroy(&pool->thread_cnt);
pthread_cond_destroy(&pool->quque_not_empty);
pthread_cond_destroy(&pool->quque_not_full);
}
free(pool);
pool=NULL;
return 0;
}
//模拟线程池中的线程,处理业务
void *process(void *arg){
printf("thread 0x%x working on task %d\n",(unsigned int)pthread_self(),*(int *)arg);
sleep(1);
printf("task %d is end\n",*(int *)arg);
return NULL;
}
int main (int argc, char *argv[]) {
//创建线程池
threadpool_t *thp=threadpool_create(3,10,100);
printf("pool inited\n");
int num[100],i;
for(i=0;i<100;++i){
num[i]=i;
printf("add task %d\n",i);
//向线程池中添加任务
threadpool_add(thp,process,(void *)&num[i]);
}
sleep(30); //模拟等待子线程完成任务
threadpool_destroy(thp); //销毁线程池
return 0;
}
UDP服务器
概念
引言:
UDP
:无连接的、不可靠的数据报传递协议,对于不稳定的网络层,采取完全不弥补的通信方式,默认还原网络状态优点
:- 传输速度快
- 效率高
- 开销小
缺点
:数据流量、速度、顺序不稳定使用场景
:对时效性要求较高场合,稳定性其次在socket编程中,我们讲了TCP的服务器实现,相对应的UDP服务器的实现也是有必要学的,因为UDP的特性也就铸就着他的应用场景,例如:游戏、视频会议、视频电话
概念:
UDP通信服务器
:基于UDP协议实现的通信服务器,根据UDP的特点,将部分的不需要的函数去除(accept和connect函数去除)使用场景
:对时效性要求较高场合,稳定性其次- 对于大厂来说,当使用TCP和各种模型搭建的服务器的速度很难再提高了,毕竟TCP的特性已经表明了,但是对于UDP,我们可以在应用层使用数据校验协议来弥补UDP的不足。
函数原型
ssize_t recvfrom(int sockfd,void *buffer,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen)
:适用于UDP的接收数据函数,替换tcp中的recv1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23#include <sys/socket.h>
ssize_t recvfrom(int sockfd,void *buffer,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen);
//参数
sockfd:接收数据或请求的套接字文件描述符,数据接收地;
buffer:缓冲区;
len:缓冲区长度;
flags:是一组标志参数,控制着函数的行为,一般都置为0;
src_addr:传出参数,数据起始地地址结构;
addrlen:数据起始地地址结构结构体长度;
//返回值
成功:返回接收到的字节数,0,表示连接已经关闭;
失败:-1,errno;ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen)
:适用于UDP的发送数据的函数,代替TCP中的send1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23#include <sys/socket.h>
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen);
//参数
sockfd:发送数据的套接字文件描述符,数据起始地;
buf:要发送的数据,也就是缓冲区;
len:缓冲区长度;
flags:是一组标志参数,控制着函数的行为,一般都置为0;
dest_addr:传入参数,数据目的地地址结构;
addrlen:数据目的地地址结构体长度;
//返回值
成功:返回发送的字节数;
失败:-1,errno;
server
实现思路:
- socket建立服务端套接字文件描述符sockfd,使用报式协议
- bind绑定服务端地址结构到服务端套接字上
- 因为UDP没有三次握手,所以取消掉listen(可有可无),connect以及accept,直接进行收发数据操作
- 使用recvfrom接收数据
- 使用sendto发送数据
- 服务端结束运行close关闭服务端套接字
注意:注意UDP使用的是报式协议,TCP使用的是流式协议,所以socket函数的第二个参数应该为SOCK_DGRAM
示例代码:
1 |
|
client
实现思路:
- socket创建客户端套接字cfd,使用报式协议
- 初始化服务端地址结构
- 与服务端直接通信,不需要连接
- 使用sendto发送数据
- 使用recvfrom接收数据
- close关闭客户端套接字
示例代码:
1 |
|
本地套接字
概念:
本地套接字(domain socket)
:本地进程间通信的一种实现方式,除了本地套接字以外,其他技术,如管道、共享信息队列等也是进程间通信的常用方法,因为本地套接字开发便捷,接受度高,所以普遍适用于同一台主机上进程间通信的各种场景利用本地套接字可完成可靠字节流和数据报两种协议。
本地套接字地址结构为sockaddr_un
sockaddr_un数据结构:
1 |
|
注意:因为sockaddr_un跟sockaddr的数据结构不同,因此我们不能再用sizeof求解地址结构长度,而是需要分别计算sun_family和sun_path的长度然后加起来即可
函数原型:
int socket(int domain,int type,int protocol)
:创建套接字函数,将domain参数设定为AF_UNIX/AF_LOCAL即可创建本地套接字1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//参数
domain:AF_UNIX/AF_LOCAL;
type:SOCK_STREAM/SOCK_DGRAM;
protocol:表示选用的协议当中代表协议,一般传0,流式传输协议代表协议为TCP,报式传输协议代表为UDP,也可以直接填IPPROTO_TCP或者IPPROTO_UDP;
//返回值
成功:新套接字所对应的文件描述符;
失败:-1 errno;size_t offsetof(type,member)
:宏函数,用于求解type结构体中的member成员变量的地址与结构体首地址的偏移量1
2
3#include <stddef.h>
size_t offsetof(type, member);
sockaddr_un长度求解:
1 |
|
服务端示例代码:
1 |
|
注意:
为了保证bind函数调用成功,我们需要在bind之前使用unlink将socket通信的文件删除掉,防止因为同名而调用失败
客户端示例代码
1 |
|
注意:
- 不能依赖于隐式绑定,因为需要socket通信文件名,所以要绑定客户端地址结构
- 客户端还需连接服务端然后通信,所以客户端需要初始化服务端地址结构,然后进行connect连接