Linux 高并发服务器实战 - 4 Linux网络编程
C/S架构可以自己定协议,B/S架构的协议是固定的:http/https (无法传递大数据量)
B/S架构是请求-响应,需要动态刷新,效率低
IP地址32位,是4个字节
IP地址最后一个字节一般不用255,它一般是用来广播的,第一个字节一般也不用0,从1开始
127 开头用于回环测试。127.0.0.1 本机
ping -> 测试某个主机能否和当前主机连通
ping 127.0.0.1 和自己连通
端口号有两个字节
IP是用来找到计算机的,但是消息是在应用之间发的,我们还得给一个定好的端口(比方QQ是用的哪个端口)
端口在计算机内存中相当于一个缓冲区,(有读缓冲区、写缓冲区)
用端口号来标识缓冲区,有65535个
进程的端口号是唯一的,为什么不用进程号来标识呢?不能,因为进程号每次都是动态产生的不是固定的
服务端回给客户端数据的时候,端口号就是动态端口(动态分配的)
端口号:这块读/写缓冲区的唯一标识
一个应用程序可以由多个端口,不同的端口可以和不同的应用程序通信
物理层要把101010的信号改变为电信号,高低电频
接收方物理层一样,要给再变回来
主流:TCP/IP四层模型,里面有TCP/IP协议族
我们就是用sshd远程连接的linux服务器(其实也是客户端和服务器的通信,它的端口号是22)
我们填写了IP地址(表明服务器),端口号(表示它那个进程)
ARP协议 : 根据IP地址找MAC地址
RARP:根据MAC找IP
协议最终体现为在网络上传输的数据包的格式
TTL每经过一个路由器 -1,一开始默认64/128 减到0就丢失了
收到的那一方,通过TCP/UDP头部里的端口号去决定发给哪个应用
分用是根据头部中的信息
接收端那边拿到,如果目的地址不是自己,那就丢掉了。
如果是自己,因为类型是0x800,所以去掉头部后交给上层的IP模块去处理
到了网络层看目的IP和本人是不是一样的,一样就处理并去掉头部,不一样就扔掉
IP头部里也有协议号,标明上一层是TCP/UDP?
到了传输层,再去找有没有对应的端口号…
传给应用
问题:到底是怎么查找到目标端的IP地址的?
ARP协议 -> 根据IP找到MAC地址
注意目的MAC地址目前为空(里面都是0)响应之后才会填充上
要想发送出去ARP请求包,前面还要加以太网帧头(6+6+2 == 14字节) 尾:4字节
A发送ARP请求,会发送给整个子网的主机,每个主机收到请求后根据里面的目的端IP去应答,A主机就拿到它们的IP地址了
FF:FF:FF:FF是给这个网络里所有主机都发
B/C主机响应的时候,源地址,目的地址的MAC地址、IP地址都是清楚了的,都能填充的上
注意操作部分由1变成2,由请求变成应答
注意在实际使用时,上层的协议使用下层的协议帮我们去完成一些任务
socket 介绍
先写数据到写缓冲区,然后封装(应用层、传输、网络、接口…) 再分用并且到了读缓冲区
套接字通信分两部分:
- 服务器端:被动接受连接,一般不会主动发起连接
- 客户端:主动向服务器发起连接
socket是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别。
字节序
传输的时候是小端,解析的时候是大端?没规定好,没法正常的解码编码
字节序:字节在内存中存储的顺序。
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
#include <stdio.h>
int main() {
//c语言中的union 是联合体,就是一个多个变量的结构同时使用一块内存区域,
//区域的取值大小为该结构中长度最大的变量的值
union {
short value; // 2字节
char bytes[sizeof(short)]; // char[2]
} test;
test.value = 0x0102;
if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
printf("大端字节序\n");
} else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
printf("小端字节序\n");
} else {
printf("未知\n");
}
return 0;
}
解决字节序问题在传输前都要先转化为大端
h - host 主机,主机字节序
to - 转换成什么
n - network 网络字节序
s - short unsigned short
l - long unsigned int
在网络上传输的网络字节序都是大端,主机字节序有可能是小端也有可能是大端
#include <arpa/inet.h>
// 转换端口 端口2个字节
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
// 转IP IP是4个字节
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序
转主机字节序的时候,如果本身是大端,就不转了,是小端才转
注意!
注意buf本身是一个char指针,把它转为int * 再对他取值,就是取了四个字节的!
怎么再单独取出每个字节的地址?转为char,再以步长为1往前走*
// htonl 转换IP
char buf[4] = {192, 168, 1, 100};
//注意buf本身是一个char指针,把它转为int * 再对他取值,就是取了四个字节的!
int num = *(int *)buf;
int sum = htonl(num);
unsigned char *p = (char *)∑
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// htons 转换端口
unsigned short a = 0x0102;
printf("a : %x\n", a);
unsigned short b = htons(a);
printf("b : %x\n", b);
printf("=======================\n");
// htonl 转换IP
char buf[4] = {192, 168, 1, 100};
int num = *(int *)buf;
int sum = htonl(num);
unsigned char *p = (char *)∑
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
printf("=======================\n");
// ntohl
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char *p1 = (unsigned char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
// ntohs
return 0;
}
输出:
=======================
100 1 168 192
=======================
192 168 1 1
socket 地址
// socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。
// 客户端 -> 服务器(IP, Port)
通用 socket 地址
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
typedef unsigned short int sa_family_t;
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议
族(protocol family,也称 domain)和对应的地址族入下所示:
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 |
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混
用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所
示:
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可达到108字节 |
PF_INET | 16 bit 端口号和 32 bit IPv4 地址,共 6 字节 |
PF_INET6 | 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节 |
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的
通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;
专用socket地址
详见pdf, 从sockaddr 转变为了 sockaddr_in
注意,最后在用的时候,还是要转化成为sockaddr才行,强转即可
通用sockaddt里有一个 sa_famliy地址族类型的变量(存常见三种协议族),以及14个字节的数据存地址及端口号等,后面又有了专用的socket地址
in_addr_t一般为32位的unsigned int,用它存IPV4
IP地址转换
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
//是把转换后的值存在inp里了,不是返回值,返回值是表示是否成功
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的 但是dst也是必须要传的
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// 创建一个ip字符串,点分十进制的IP地址字符串
char buf[] = "192.168.1.4";
unsigned int num = 0;
// 将点分十进制的IP字符串转换成网络字节序的整数
inet_pton(AF_INET, buf, &num);
unsigned char * p = (unsigned char *)#
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
// 将网络字节序的IP整数转换成点分十进制的IP字符串
// 定义16是因为 3 * 4 = 12 再加三个点 再加一个\0 -> 16
char ip[16] = "";
const char * str = inet_ntop(AF_INET, &num, ip, 16);
printf("str : %s\n", str);
printf("ip : %s\n", str);
// 注意返回值也是转换后的值,传入的参数ip 它里面对应的值也是改变了的
printf("%d\n", ip == str);
return 0;
TCP 通信流程
// TCP 和 UDP -> 传输层的协议
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
UDP TCP
是否创建连接 无连接 面向连接
是否可靠 不可靠 可靠的
连接的对象个数 一对一、一对多、多对一、多对多 支持一对一
传输的方式 面向数据报 面向字节流
首部开销 8个字节 最少20个字节
适用场景 实时应用(视频会议,直播) 可靠性高的应用(文件传输)
// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接
监听socket fd里面有没有数据,对面的写缓冲区写上,我们读入到读缓冲区,发现有了,就表示监听到了
accept 一开始阻塞,监听到有fd连接进来,才会放开
监听是监听的文件描述符,accept之后是返回新的fd,这个和客户端的进行通信,而不是用监听的去通信,它还得继续监听呢。等又有了新的,又accept,又建立新的fd,它得用于一直监听
// 客户端
1. 创建一个用于通信的套接字(fd) (它不用绑定IP和端口号,三次握手之后双方的IP和端口就都知道了)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接
bind() 是把IP 和 Port 绑定到 上面监听socket fd上(需要有IP 和 Port 来标识套接字)
bind() 也可以称作socket命名
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5 (不需要指定太大)
(有未连接和已经连接的队列,accept 一但接收,已经连接的队列里就没有它了,这个执行的速度是非常快的,所以不用担心这个数字很小)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符(需要从读缓冲区里拿出来客户端的IP和Port)
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
TCP通信实现(服务器端)
计算机有多个网卡,以太网卡,无线网卡,他们的IP地址是不一样的,为保证他们都能访问到
那给socket绑定地址sockaddr_in时,将其IP(sin_addr.s_addr)设置为0(INADDR_ANY),就都能访问到了
// TCP通信:服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//1.创建用于监听的socket
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(-1);
}
//2.绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1)
{
perror("bind");
exit(-1);
}
//3.监听
ret = listen(lfd, 8);
if(ret == -1)
{
perror("listen");
exit(-1);
}
//4.接收客户端连接
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
if(cfd == -1)
{
perror("accept");
exit(-1);
}
//输出客户端信息
char clientIP[16];
//将网络字节序的整数,转化成点分十进制的字符串
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n",clientIP, clientPort);
//5.通信
char recvBuf[1024] = {0};
while (1)
{
int num = read(cfd, recvBuf, sizeof(recvBuf));
if(num == -1)
{
perror("read");
exit(-1);
}else if(num > 0){
printf("recv client data : %s\n", recvBuf);
}else if(num == 0){
//表示客户端断开连接
printf("client closed..");
break;
}
char* data = "hello, i am server";
write(cfd, data, strlen(data));
}
//关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
TCP通信实现(客户端)
// TCP通信 客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(-1);
}
//2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.0.74", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if(ret == -1)
{
perror("connect");
}
//3.通信(不发数据的话,服务器端的read那里又会卡住)
char recvBuf[1024] = {0};
while (1)
{
char* data = "hello, i am client";
write(fd, data, strlen(data));
sleep(1);
int num = read(fd, recvBuf, sizeof(recvBuf));
if(num == -1)
{
perror("read");
exit(-1);
}else if(num > 0){
printf("recv server data : %s\n", recvBuf);
}else if(num == 0){
//表示服务器端断开连接
printf("server closed..");
break;
}
}
close(fd);
return 0;
}
TCP三次握手
三次握手的目的是保证双方互相之间建立了连接
三次握手发生在客户端建立连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。
SYN -> 建立连接
ACK -> 确认
客户端和服务端都要确认双方能够收发信息
最后一次握手是为了让服务端确认 客户端可以收到数据
所以,为保证连接的可靠性至少是三次握手,两次握手不行
第一次:
S知道:S可收、C可发
C知道:C可发
第二次:
S知道: S可收可发、C可发
C知道:S可收可发、C可发可收
第三次:
S知道:S可收可发、C可发可收
序号:为每一个字节生成一个序号
确认序号
可以通过序号和确认序号保证数据的完整以及顺序
如何确保数据完整?
从110开始发,发了3个 seq = 110(3) , ack = 113 表示这收到了,并且下次希望从113开始收
注意
只有发送SYN / FIN时,确认序号是+1,其余的都是加具体数据的长度
一开始的序号seq是随机的
注意cack 后面一直是2001,因为服务端并没有再发数据
cack是客户端希望服务端下次从2001开始发
sack是服务端希望客户端下次从哪里开始发
(一开始三次握手的时候是没发数据的,只是SYN和ACK的响应)
第一次握手:
1.客户端将SYN标志位置为1
2.生成一个随机的32位的序号seq=J, 这个序号后边是可以携带数据(数据的大小)
第二次握手:
1.服务器端接收客户端的连接, ACK = 1
2.服务器端回发一个确认号序号,ack = 客户端序号 + 数据长度 + SYN/FIN(1个长度)
3.服务器端向客户端发起连接请求:SYN = 1
4.服务器端生成一个随机序号 seq = K
第三次握手:
1.客户端应答服务器的连接请求,ACK = 1
2.客户端回发一个确认序号,ack = 服务器端序号 + 数据长度 +SYN/FIN(1)
!!!注意:只有第三次的时候是可以带一些数据的
滑动窗口
窗口理解为缓冲区的大小
滑动窗口的大小会随着发送数据和接收数据而变化
通信的双方都有发送缓冲区和接收数据的缓冲区
服务器:
发送缓冲区(发送缓冲区的窗口)
接收缓冲区(接收缓冲区的窗口)
客户端:
发送缓冲区(发送缓冲区的窗口)
接收缓冲区(接收缓冲区的窗口)
发送方的缓冲区
白色格子:空闲空间
灰色格子:数据已经被发送出去了,但是还没有被接收(因为还没有收到ACK)
紫色格子:还没有发送出去的数据
接收方的缓冲区:
白色格子:空闲的空间
紫色格子:已经接受到的数据
接收缓冲区会告诉发送缓冲区我还剩多大,控制他那边发送的数量
# mss : Maximum Segment Size(一条数据的最大数据量)
# win : 滑动窗口
看4-9 是发送端往接收端发了6次,每次大小为1024 也就是1K,接收端滑动窗口满了
看10:接受到了前6145个数据,现在win窗口大小是2048(也就是说,我窗口里处理了2K了)
看11:win4096:处理了4K了,窗口现在是4096
1. 客户端向服务器发起连接,客户端的滑动窗口是4096,一次发送的最大数据量是1460
2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
3.第三次握手
4.4-9
5.第10次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
6.第11次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
7.第12次,客户端给服务器发送了1k的数据
8.第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1K的数据
9.第14次,注意回复ack是8194不是8193(因为加了一个FIN),
10.第15、16次,通知客户端滑动窗口的大小
11.第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12.第18次,第四次挥手,客户端同意了服务器端断开的请求
四次挥手
四次挥手发生在断开连接的时候,在程序当中调用了close()会使用TCP协议进行四次挥手。
会释放IP、port...那些信息,以免占用内存资源
客户端和服务器端都可以主动发起断开连接,谁先调用close()就是谁发起
因为TCP在建立连接的时候,采用三次握手建立的连接是双向的,在断开的时候需要双向断开
注意主动断开连接的一方,断开之后不能发送数据了,但是还是可以接收数据的
为什么四次一方收到另一方FIN并发送ACK后不一并把自己的FIN发过去,变成三次挥手?因为你和我断开连接我同意了,但是我还没有要和你断开连接
TCP通信并发
要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。
思路:
1.一个父进程,多个子进程
2.父进程负责等待并接受客户端的连接
3.
注意:
对于往socket里写数据:
- 要么给往里写数据的字符串数组初始化为{0}
- 要么在write的时候,把长度改为strlen(recvBuf) + 1,带上那个’\0’的位置 (使用这个)
另外不能再while循环里进行父进程对子进程的回收,一有wait,整个阻塞在那了,不能接收另一个客户端
用waitpid也不太好,所以我们用信号来完成释放
注意处理信号的handler函数要用while循环起来,搭配waitpid使用,有可能死了三个子进程,但是未决信号只有一个,我们用waitpid 一次是处理不完三个的
软中断
收到信号中断了,等再回来之后accept就不阻塞了,目前没新的客户端连接,它就报错了,所以我们处理一下
如果中断了(errno是 EINTR,那么就continue)
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) {
continue;
}
perror("accept");
exit(-1);
}
整体代码:
client:
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024];
int i = 0;
while(1) {
sprintf(recvBuf, "data : %d\n", i++);
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
sleep(1);
}
// 关闭连接
close(fd);
return 0;
}
server:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
void recyleChild(int arg) {
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1) {
// 所有的子进程都回收了
break;
}else if(ret == 0) {
// 还有子进程活着
break;
} else if(ret > 0){
// 被回收了
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main() {
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 不断循环等待客户端连接
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
if(errno == EINTR) {
continue;
}
perror("accept");
exit(-1);
}
// 每一个连接进来,创建一个子进程跟客户端通信
pid_t pid = fork();
if(pid == 0) {
// 子进程
// 获取客户端的信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(cliaddr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(cfd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(cfd, recvBuf, strlen(recvBuf) + 1);
}
close(cfd);
exit(0); // 退出当前子进程
}
}
close(lfd);
return 0;
}
多线程实现并发服务器
线程的函数的参数是要以指针的形式传入,因为我们要传入很多,所以我们把它打包成一个结构体传入
又因为while函数里的变量是局部变量,我们传入线程函数后,它指针指向的内容已经释放了,所以我们把它存在堆里(用malloc),但是在子线程的函数结束后要把这部分空间释放。如果要有10000个线程同时连接,开销也是很大的
另外一种,我们可以把他们存在一个结构体数组里,记得要对他们初始化,fd初始化为-1,tid初始化为-1,表示他们是可用的
// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
整体代码:
#define _XOPEN_SOURCE
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <pthread.h>
struct sockInfo
{
int fd; //通信的文件描述符
pthread_t tid; //线程号
struct sockaddr_in addr;
};
struct sockInfo sockinfos[128];
void* working(void* arg)
{
//子线程和客户端通信 要用到的参数:cfd 、客户端的信息、线程号
//因为这些参数要以一个指针的形式传进来,所以我们需要定义一个结构体
struct sockInfo * pinfo = (struct sockInfo *)arg;
char cliIp[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntohs(pinfo->addr.sin_port);
printf("client ip is : %s, prot is %d\n", cliIp, cliPort);
// 接收客户端发来的数据
char recvBuf[1024];
while(1) {
int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
}else if(len > 0) {
printf("recv client : %s\n", recvBuf);
} else if(len == 0) {
printf("client closed....\n");
break;
}
write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
}
close(pinfo->fd);
return NULL;
}
int main()
{
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
bzero(&sockinfos[i], sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
struct sockInfo * pinfo;
for(int i = 0; i < max; i++) {
// 从这个数组中找到一个可以用的sockInfo元素
if(sockinfos[i].fd == -1) {
pinfo = &sockinfos[i];
break;
}
if(i == max - 1) {
sleep(1);
i--;
}
}
pinfo->fd = cfd;
//注意这里结构体数据的复制是要用memcpy
memcpy(&pinfo->addr, &cliaddr, len);
// 创建子线程
pthread_create(&pinfo->tid, NULL, working, pinfo);
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
TCP状态转换
没有接收到最后一次ACK,不是完整的断开连接
他会等2MSL,如果要没接受到,另一方会再发FIN,这一方再发一次ACK(丢失重发)。
2MSL之后这一端就关闭了
半关闭
从程序的角度,可以使用 API 来控制实现半连接状态:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发
出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以
SHUT_WR。
半关闭时:A不能写了,但是还能读。但是如果用close,就整个都关了,读写都不行。
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用
计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方
向的连接,只中止读或只中止写。
注意:
- 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用
进程都调用了 close,套接字将被释放。 - 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。
但如果一个进程 close(sfd) 将不会影响到其它进程。
端口复用
查看网络相关的命令:
netstat
参数:
-a 所有的socket
-p 显示正在使用socket的程序的名称
-n 直接使用IP地址,不通过域名服务器
服务器重启了 /Ctrl + C停了、端口号还没有释放,此时再次启动服务器,他会报错说这个端口已经被用了,所以才使用了端口复用
这个时候是处于TIME_WAIT(虽然另一方还没给他发FIN)/FIN_WAIT_2阶段
端口复用最常用的用途是:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
//不仅仅能设置端口复用,还可以设置套接字的属性
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t
optlen);
参数:
-sockfd : 要操作的文件描述符
-level: 级别 - SOL_SOCKET (端口复用的级别)
-optname: 选项的名称
- SO_REUSEADDR
- SO_REUSEPORT
-optval : 端口复用的值(整形)
- 1 : 可以复用
- 0 : 不可以复用
-optlen : optval参数的大小
端口复用,设置的时机是在服务器端口绑定前
IO多路复用
输入:从文件中把数据写到内存中
输出:从内存中把数据写到文件中
I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。
下面是server的fd使用情况,上面是client的fd使用情况
socket相当于流缓冲区,都通过各自socket来获取流中的数据。**2端socket通过一条固定“电话线”进行通讯,即Client端选择跟哪台Server,哪个端口建立连接;作为Server端只监听相应的端口。**在这里,Client处于主动。
建立连接后,两端都可以使用流通过socket相互的发送信息和接受信息。两端的socket都会不断刷新socket里的内容。
提高了程序执行效率(不用阻塞了),缺点是需要更多资源(你要不断地打电话)
1W个 Client程序,但是只有1个到达了数据,其余的9999次非阻塞的read(要把read套在循环里)都是无效的。
比方说100个文件描述符(对应100个缓冲区),内核会去看它们谁的读缓冲区有内容了,然后提醒说有几个有内容了,你自己再去遍历看是哪几个有了。(会把标志位设置为1 )
select
主旨思想:
- 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
- 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O
操作时,该函数才返回。
a.这个函数是阻塞
b.函数对文件描述符的检测的操作是由内核完成的- 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
// sizeof(fd_set) = 128bytes 1024bits (1024个标志位的文件描述符)
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 参数:
- nfds : 委托内核检测的最大文件描述符的值 + 1
- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
- 一般检测读操作
- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
- 是一个传入传出参数(检测完之后就把0/1修改了)
- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
不满置为1可以写,满了是0不可写
- exceptfds : 检测发生异常的文件描述符的集合
- timeout : 设置的超时时间
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
- NULL : 永久阻塞,直到检测到了文件描述符有变化
- tv_sec = 0 tv_usec = 0, 不阻塞
- tv_sec > 0 tv_usec > 0, 阻塞对应的时间
- 返回值 :
- -1 : 失败
- >0(n) : 检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);
+1是因为要遍历到它
内核发现 A、B 发送的数据到了,所以把3和4设置为1,但是没检测到100和101的数据,所以把他们置为0了
(有一个从用户态拷贝到内核态、再从内核态拷贝到用户态的一个过程)
一开始是有这四个的连接的,所以给他们先在用户态里设置1
服务器:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main() {
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
//绑定
bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
//监听
listen(lfd, 8);
//BIO的效率是非常低的
fd_set rdset, tmp; //rdset只允许用FD_SET、FD_CLR、FD_ZERO修改,tmp用内核修改
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd; //一开始就是012、3(监听)
while(1)
{
//rdset 每次只是被我自己修改,它指定了内核需要去检查哪些fd
//不会在内核里改过改成0了,下次就没法再检查那个fd了,检查哪个由rdset决定
tmp = rdset;
//调用select函数,让内核帮检测哪些文件描述符有数据
//超时时间为NULL表示永久阻塞,直到内核检测出哪些发生了变化才会不阻塞
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if(ret == -1)
{
perror("select");
exit(-1);
}else if(ret == 0){
//没有检测到
continue;
}else if(ret > 0){
//说明检测到了有文件描述符对应的缓冲区的数据发生了改变
//分两步判断
if(FD_ISSET(lfd, &tmp))
{
//有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
//将新的文件描述符加入到fdset
FD_SET(cfd, &rdset);
maxfd = maxfd > cfd ? maxfd : cfd;
}
//看其他的文件描述符有没有变化
for(int i = lfd + 1; i <= maxfd; i++)
{
if(FD_ISSET(i, &tmp)){
//说明文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1)
{
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
//断开连接了,不用检查了,把它从检查的列表里拿出来
close(i);//记得关掉
FD_CLR(i, &rdset);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
客户端:
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "192.168.0.74", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "send data %d", num++);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
// sleep(1);
usleep(1000);
}
close(fd);
return 0;
}
poll
#include <poll.h>
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 */ 要检测的事件
short revents; /* 文件描述符实际发生的事件 */ 内核返回的事件
};
// 可以重用、且无1024的限制、内核修改的是revents
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数:
- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
- timeout : 阻塞时长
0 : 不阻塞
-1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
>0 : 阻塞的时长
- 返回值:
-1 : 失败
>0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化
服务器:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
int main() {
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
//绑定
bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
//监听
listen(lfd, 8);
//初始化检测的文件描述符数组
struct pollfd fds[1024];
for(int i = 0; i < 1024; i++)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int nfds = 0;
while(1)
{
//调用poll系统函数,让内核帮忙检测哪些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if(ret == -1)
{
perror("poll");
exit(-1);
}else if(ret == 0){
//没有检测到
continue;
}else if(ret > 0){
//说明检测到了有文件描述符对应的缓冲区的数据发生了改变
//分两步判断
if(fds[0].revents & POLLIN)
{
//有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
//将新的文件描述符加入到fdset
for(int i = 1; i < 1024; i++)
{
if(fds[i].fd == -1)
{
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
nfds = nfds > cfd ? nfds : cfd;
}
//看其他的文件描述符有没有变化
for(int i = 1; i <= nfds; i++)
{
if(fds[i].revents & POLLIN){
//说明文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int len = read(fds[i].fd, buf, sizeof(buf));
if(len == -1)
{
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
//断开连接了,不用检查了,把它从检查的列表里拿出来
//注意是要先close, 再置为-1
close(fds[i].fd);
fds[i].fd = -1;
}else if(len > 0){
printf("read buf = %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
epoll
epoll的效率要比select和poll高
eventpoll是在内核区中的数据结构,它的返回值是文件描述符,然后我们去操作它
select/poll是要创建一个fd_set或者数组,把它传递给select/poll,然后从用户态拷贝到内核态
这里是直接建立到内核态,且这里的数据结构效率更高,原来是线性的数据结构,这里是红黑树
之前select/poll是把整个要拷贝回去,但这里rdlist拷贝很快,就只是拷贝就绪的
#include <sys/epoll.h>
// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。
int epoll_create(int size);
- 参数:
size : 目前没有意义了。随便写一个数,必须大于0
- 返回值:
-1 : 失败
> 0 : 文件描述符,操作epoll实例的
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数:
- epfd : epoll实例对应的文件描述符
- op : 要进行什么操作
EPOLL_CTL_ADD: 添加 (添加到红黑树里)
EPOLL_CTL_MOD: 修改
EPOLL_CTL_DEL: 删除
- fd : 要检测的文件描述符
- event : 检测文件描述符什么事情(读?写?)
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数:
- epfd : epoll实例对应的文件描述符
- events : 传出参数,保存了发送了变化的文件描述符的信息
- maxevents : 第二个参数结构体数组的大小
- timeout : 阻塞时间
- 0 : 不阻塞
- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
- > 0 : 阻塞的时长(毫秒)
- 返回值:
- 成功,返回发送变化的文件描述符的个数 > 0
- 失败 -1
注意如果上面的事件还包括了EPOLLOUT写,那么下面捕获到其他curfd的时候,如果我们是只是针对读事件,我们还需要单独处理
if(curfd == lfd){
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
epev.events = EPOLLIN | EPOLLOUT;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}
else
{
if(epevs[i].events & EPOLLOUT)
{
...
}
}
服务端代码:
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
int main()
{
//创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
//绑定
bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
//监听
listen(lfd, 8);
//调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
//将监听的文件描述符相关的检测信息添加到epoll实例当中(加到rbr里)
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while (1)
{
//把已经就绪的、有数据发生改变的文件描述符的信息放在里面
//1024是前一个参数的大小?
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1)
{
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++)
{
int curfd = epevs[i].data.fd;
if(curfd == lfd){
//监听的文件描述符有数据到达,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
epev.events = EPOLLIN | EPOLLOUT;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}else{
//有数据到达了,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1)
{
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed...\n");
//从红黑树里面删除掉
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}else if(len > 0){
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
EPOLL的两种工作模式
Epoll 的工作模式:
- LT 模式 (水平触发)(只要缓冲区有数据还没读,就回同意)
假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
b.用户只读了一部分数据,epoll会通知
c.缓冲区的数据读完了,不通知
LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。
- ET 模式(边沿触发)共有8个字节,你只读了两个字节,下次就不通知了,你读不到后面的了,所以只能循环地去读,但是如果循环地读,read会阻塞,所以这里用非阻塞套接字接口
假设委托内核检测读事件 -> 检测fd的读缓冲区
读缓冲区有数据 - > epoll检测到了会给用户通知
a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
b.用户只读了一部分数据,epoll不通知
c.缓冲区的数据读完了,不通知
ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
比如定义服务器从缓冲区里读的buf大小为5,那输入一个字符串要从缓冲区里读好几次才能读完,对于lt来说,读不完它会一直提醒,一直调用epoll_wait,一直循环
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信
char buf[5] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
EPOLL_ET
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET
在哪里设置边沿触发?
epev.events = EPOLLIN | EPOLLET;
这样的话有数据只通知一次
int ret = epoll_wait(epfd, epevs, 1024, -1);
//再次while过来这个函数它是不会通知你的,epvs里是没给你添加内容的
//虽然缓冲区里还有数据,但是就阻塞在这里了
这个时候你再在客户端输入一些内容发送,它这边会继续从缓存里读五个(因为有了新数据进来),有了新的信号,但是仍然读一次读不完,读一次就卡在这了
解决:一次性把数据读出来
注意read阻塞与否是由文件描述符决定
// 设置cfd属性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag | O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
注意上面的服务器的代码里,read完之后再write的数据是回写的数据
有关这里使用循环非阻塞read的注意:
len==-1时不能直接退出,如果是因为读完了等于-1(EAGAIN),还要继续再次循环
else if(len == -1) {
//注意等于-1时,不能直接退出,等于-1包括一种情况是读完了,但是后面还要继续再读的所以
if(errno == EAGAIN) {
printf("data over.....");
}else {
perror("read");
exit(-1);
}
}
整体ET代码(服务端):
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
// 设置cfd属性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag | O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
epev.events = EPOLLIN | EPOLLET; // 设置边沿触发
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 循环读取出所有数据
char buf[5];
int len = 0;
while( (len = read(curfd, buf, sizeof(buf))) > 0) {
// 打印数据
// printf("recv data : %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(curfd, buf, len);
}
if(len == 0) {
printf("client closed....");
}else if(len == -1) {
//注意等于-1时,不能直接退出,等于-1包括一种情况是读完了,但是后面还要继续再读的所以
if(errno == EAGAIN) {
printf("data over.....");
}else {
perror("read");
exit(-1);
}
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
谁先用CTRL C停止,谁要处于TIMEWAIT阶段一分钟
UDP通信
#include <sys/types.h>
#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 : 通信的fd
- buf : 要发送的数据
- len : 发送数据的长度
- flags : 0
- dest_addr : 通信的另外一端的地址信息
- addrlen : 地址的内存大小
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
- 参数:
- sockfd : 通信的fd
- buf : 接收数据的数组
- len : 数组的大小
- flags : 0
- src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL
- addrlen : 地址的内存大小
UDP不需要多线程/进程就可以实现多个客户端访问服务器
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 服务器的地址信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.0.74", &saddr.sin_addr.s_addr);
int num = 0;
// 3.通信
while(1) {
// 发送数据
char sendBuf[128];
sprintf(sendBuf, "hello , i am client %d \n", num++);
//要给上服务端的地址
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));
// 接收数据
int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
printf("server say : %s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
服务器:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
// 2.绑定
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.通信
while(1) {
char recvbuf[128];
char ipbuf[16];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接收数据
int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);
printf("client IP : %s, Port : %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
ntohs(cliaddr.sin_port));
printf("client say : %s\n", recvbuf);
// 发送数据
// cliaddr -> 要给接收端的那个地址
sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
}
close(fd);
return 0;
}
广播
向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
a.只能在局域网中使用。
b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。
// 设置广播属性的函数
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
- sockfd : 文件描述符
- level : SOL_SOCKET
- optname : SO_BROADCAST
- optval : int类型的值,为1表示允许广播
- optlen : optval的大小
广播的时候,服务端发送数据,客户端需要绑定IP和端口才能接收数据
服务器指定的cliaddr地址是192.168.0.255
客户端是绑定了本地的网卡IP地址
服务器:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.设置广播属性
int op = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));
// 3.创建一个广播的地址
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.0.255", &cliaddr.sin_addr.s_addr);
// 3.通信
int num = 0;
while(1) {
char sendBuf[128];
sprintf(sendBuf, "hello, client....%d\n", num++);
// 发送数据
sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
printf("广播的数据:%s\n", sendBuf);
sleep(1);
}
close(fd);
return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
struct in_addr in;
// 2.客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.通信
while(1) {
char buf[128];
// 接收数据
int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("server say : %s\n", buf);
}
close(fd);
return 0;
}
组播(多播)
单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。
a.组播既可以用于局域网,也可以用于广域网
b.客户端需要加入多播组,才能接收到多播的数据
组播地址
IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从 224.0.0.0 到 239.255.255.255 ,并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类:
服务器端设置多播属性,客户端加入到多播组
int setsockopt(int sockfd, int level, int optname,const void *optval,socklen_t optlen);
// 服务器设置多播的信息,外出接口
- level : IPPROTO_IP
- optname : IP_MULTICAST_IF
- optval : struct in_addr
// 客户端加入到多播组:
- level : IPPROTO_IP
- optname : IP_ADD_MEMBERSHIP
- optval : struct ip_mreq
struct ip_mreq
{
/* IP multicast address of group. */
struct in_addr imr_multiaddr; // 组播的IP地址
/* Local IP address of interface. */
struct in_addr imr_interface; // 本地的IP地址
};
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
本地套接字
本地套接字的作用:本地的进程间通信
有关系的进程间的通信
没有关系的进程间的通信
本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程。
// 本地套接字通信的流程 - tcp
// 服务器端
1. 创建监听的套接字
int lfd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字绑定本地的套接字文件 -> server端
struct sockaddr_un addr;
// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
bind(lfd, addr, len);
3. 监听
listen(lfd, 100);
4. 等待并接受连接请求
struct sockaddr_un cliaddr;
int cfd = accept(lfd, &cliaddr, len);
5. 通信
接收数据:read/recv
发送数据:write/send
6. 关闭连接
close();
// 客户端的流程
1. 创建通信的套接字
int fd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字绑定本地的IP 端口
struct sockaddr_un addr;
// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
bind(lfd, addr, len);
3. 连接服务器
struct sockaddr_un serveraddr;
connect(fd, &serveraddr, sizeof(serveraddr));
4. 通信
接收数据:read/recv
发送数据:write/send
5. 关闭连接
close();
客户端和服务器绑定的套接字文件是伪文件,真正数据还是在内存缓冲区中
写的话是写在自己的写缓冲区里,然后它就到了对方的读里面,对方去读
在用bind绑定地址的时候绑定的是本地sockaddr_un的文件,它里面的sun_path是".sock"文件,会新建
客户端和服务器都要用bind绑定本地套接字文件(代替绑定IP、Port)
服务器
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>
int main() {
unlink("server.sock");
// 1.创建监听的套接字
int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "server.sock");
int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.监听
ret = listen(lfd, 100);
if(ret == -1) {
perror("listen");
exit(-1);
}
// 4.等待客户端连接
struct sockaddr_un cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
printf("client socket filename: %s\n", cliaddr.sun_path);
// 5.通信
while(1) {
char buf[128];
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("client closed....\n");
break;
} else if(len > 0) {
printf("client say : %s\n", buf);
send(cfd, buf, len, 0);
}
}
close(cfd);
close(lfd);
return 0;
}
客户端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>
int main() {
unlink("client.sock");
// 1.创建套接字
int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(cfd == -1) {
perror("socket");
exit(-1);
}
// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "client.sock");
int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
perror("bind");
exit(-1);
}
// 3.连接服务器
struct sockaddr_un seraddr;
seraddr.sun_family = AF_LOCAL;
strcpy(seraddr.sun_path, "server.sock");
ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 4.通信
int num = 0;
while(1) {
// 发送数据
char buf[128];
sprintf(buf, "hello, i am client %d\n", num++);
send(cfd, buf, strlen(buf) + 1, 0);
printf("client say : %s\n", buf);
// 接收数据
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == -1) {
perror("recv");
exit(-1);
} else if(len == 0) {
printf("server closed....\n");
break;
} else if(len > 0) {
printf("server say : %s\n", buf);
}
sleep(1);
}
close(cfd);
return 0;
}