4.1 网络结构模式
4.1.1 C/S结构
4.1.2 B/S结构
4.2 MAC地址、IP地址、端口(1)(4.3 MAC地址、IP地址、端口(2))
4.2.1 MAC 地址


4.2.2 IP地址
4.2.3 端口
4.4 网络模型
4.4.1 OSI 七层参考模型
4.4.2 TCP/IP 四层模型
4.5 协议
4.5.1 协议简介及常见协议
4.5.2 UDP协议
4.5.3 TCP协议
4.5.4 IP协议
4.5.5 以太网帧协议
4.5.6 ARP协议
4.6 网络通信协议的过程(1)
4.6.1 封装
4.6.2 分用
当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数据, 以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing)。 分用是依靠头部信息中的类型字段实现的。
4.6.3 网络通信的过程模拟
模拟:将主机A(ip地址:192.168.1.1/24;MAC地址:01:02:03:04:05:06)中的QQ信息发送给主机B(ip地址:192.168.1.2/24;MAC地址:0a:0b:0c:0d:0e:0f)
4.7 网络通信协议的过程(2)
对 4.6.3 中的模拟网络通信过程中的一些问题:在数据链路层进行通信的时候,怎么寻找B的MAC地址?
- 使用ARP协议(arp -a),可以根据ip找到MAC地址。
例,
组建一个arp请求包:
4.8 socket介绍
所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处 的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口, 是应用程序与网络协议根进行交互的接口。 socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用 层进程传送数据包的机制。 socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为 内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接 字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文 件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字通信分两部分:
- 服务器端:被动接受连接,一般不会主动发起连接
- 客户端: 主动向服务器发起连接
socket 是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别。
4.9 字节序
4.9.1 简介
现代 CPU 的累加器一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数。那么这 4 字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机 体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问 题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
4.9.2 字节序举例
4.9.3 通过代码判断字节序类型
/*
字节序:字节在内存中存储的顺序
小端字节序:数据的高位字节存储在内存的高地址处,低位字节存储在内存的低位地址处
大端字节序:数据的高位字节存储在内存的低地址处,低位字节存储在内存的高位地址处
*/
// 通过代码检测当前主机的字节序
#include <stdio.h>
int main(){
union {
short value; // 2字节
char bytes[sizeof(short)]; // char[2]
} test;
test.value = 0x0102; // 16进制的2个字节(01 02)
// 判断:低位是否为01,高位是否为02
/*
***************
* 0x01 * 0x02 * 此时01在低位,02在高位。为大端字节序
***************
*/
if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
printf("大端字节序\n");
}else if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
printf("小端字节序\n");
}else {
printf("未知\n");
}
return 0;
}
4.10 字节序转换函数
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决问题的 方法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数 据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小 端机转换,大端机不转换)。
网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而 可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。 BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数: htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
h -- host主机,主机字节序
to - 转换成……
n -- network 网络字节序
s -- short unsigned short
l -- long unsigned int
头文件 #include<arpa/inet.h>
// 转换端口(port占2个字节,用short类型)
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntons(uint16_t netshort); //
// 转ip(ip占4个字节,用long类型)
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_ntohl(uint32_t netlong); // 主机字节序 - 网络字节序
/*
网络通信时,需要将主机字节序转换成网络字节序(大端)
另一端获取到数据后,根据情况将网络字节序转换成主机字节序
*/
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// htons 转换端口
unsigned short a = 0x0102;
unsigned short b = htons(a);
printf("a: %x\n",a);
printf("b: %x\n",b);
printf("=============\n");
// 转换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));
// 结果为 100 1 168 192
printf("=============\n");
// ntohl
unsigned char buf1[4] = {1, 1, 168, 192};
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char *p1 = (unsignwed char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
// 结果为 192 168 1 1
// ntohs -- 扩展
return 0;
}
4.11 socket地址
4.11.1 简介
socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个 socket地址。
客户端 --> 服务器(IP,port)
4.11.2 通用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)和对应的地址族入下所示:
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
由上表可知,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) ]; // 存储具体的ip数据 }; typedef unsigned short int sa_family_t;
(真正开发的时候不会使用这个通用socket地址,按字节操作会比较麻烦)
4.11.3 专用socket地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是 sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
UNIX 本地域协议族使用如下专用 socket 地址结构体:
#include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; char sun_path[108]; };
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:
#include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */ in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; struct in_addr { in_addr_t s_addr; }; struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; /* Transport layer port # */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* IPv6 scope-id */ }; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef uint16_t in_port_t; typedef uint32_t in_addr_t; #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地 址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
4.12 IP地址转换函数
字符串IP <--> 整数
主机、网络字节序之间的转换
4.12.1 相关函数操作
在编程中我们需要先把点分十进制表示的IP地址转换成整数(二进制)使用。而记录日志时又要将整数表示的IP地址转换成可读的字符串。
用于点分十进制字符串表示的IP地址 和 用于网络字节序整数表示的IP地址之间进行转换的函数:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
功能:转换成int类型的整数(网络字节序整数)
int inet_aton(const char *cp, struct in_addr *inp); // address - to - network
功能:
将字符串地址转换成网络字节序的整数
参数:
struct in_addr *inp -- 用于保存转换后的数据
返回值:
1 -- 转换成功;
0 -- 非法,错误号
char *inet_ntoa(struct in_addr in);
功能:网络字节序整数转换成字符串
/****************************了解*******************************/
// 上面三个函数比较旧,是不可重用的函数,且只适用于IPv4
// (更新)以下这对函数也能完成前面3个函数同样的功能,并且同时使用IPv4和IPv6
/****************************掌握*******************************/
#include <arpa/inet.h>
// p:点分十进制的IP字符串; n -- network,网络字节序整数
int inet_pton(int af, const char *src, void *dst);
功能:将点分十进制的IP地址 转换成 网络字节序
参数:将点分十进制的IP地址 转换成 网络字节序的整数
af -- 地址族 address family:AF_INET,AF_INET6
src -- 需要转换的点分十进制的IP字符串
*dst -- 转换后的结果保存在这个里面。传出参数
返回值:
1 -- 成功
0 -- 非法
-1 -- 错误,设置错误号
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
功能:将网络字节序的整数 转换成 点分十进制的IP地址(字符串形式)
参数:
af -- 地址族:AF_INET,AF_INET6
src -- 要转换的IP的整数的地址
dst -- 转换成IP地址字符串后保存数据的地方
size -- 第三个参数的大小(数组的容量)
返回值:返回转换后的地址(字符串),和 dst 是同一个值
4.12.2 简单案例(ip转换)
// ip转换
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// 创建一个ip字符串
char buf[] = "192.168.1.4"; // 点分十进制字符串
unsigned int num = 0;
// 将点分十进制ip转换成网络字节序的整数
inet_pton(AF_INET, buf, &num);
unsigned char * p = (unsiged char *)#
printf("%d %d %d %d", *p, *(p+1), *(p+2), *(p+3)); // 检测是否转换成unsigned int型
// 将网络字节序的IP整数转换成点分十进制的IP字符串
char ip[16] = "";
const char * str = inet_ntop(AF_INET, &num, ip, 16);
printf("str: %s\n", str);
printf("ip: %s\n", ip);
printf("%d", ip == str); // 输出结果为1 true
return 0;
}
4.13 TCP通信流程
4.13.1 TCP和UDP的区别
传输层协议
UDP:用户数据报协议,面向无连接,可以单播、多播、广播。面向数据报的协议(发送的是数据报),不可靠
TCP:传输控制协议,面向连接。可靠。基于字节流,仅支持单播传输。
UDP | TCP | |
是否创建连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠 | 可靠 |
连接对象的个数 | 一对一、一对多、多对一、多对多 | 仅支持一对一 |
传输方式 | 面向数据报 | 面向字节流 |
首部开销 | 8字节 | 最少20个字节 |
使用场景 | 实时应用(视频会议、直播) | 可靠性高的应用(文件传输) |
4.13.2 TCP通信流程
// TCP 通信的流程
// 服务器端
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听的文件描述符和本地IP及端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器时,使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,会解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
5. 通信
- 接受数据
- 发送数据
6. 通信结束,断开连接
// 客户端
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定服务器的IP和端口
3. 连接成功了,客户端可以直接和服务器通信
- 接受数据
- 发送数据
4. 通信结束,断开连接
4.14 socket函数
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 若包含了该头文件,上面两个头文件就可以省略
int socket(int domain, int type, int protocol);
功能:创建一个套接字
参数:
domain -- 协议族AF
AF_NET -- 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 -- 第二个参数结构体占的内存大小
返回值:
0 -- 成功
-1 -- 失败,并设置错误号
int listen(int sockfd, int backlog);
功能:监听这个socket上的连接
参数:
sockfd -- 通过socket()函数得到的文件描述符
backlog -- 未连接的和已经连接的和的最大值,一般5就可以了(不能超过后面命令看到的数字最大值) // vim /proc/sys/net/core/somaxconn
返回值:
0 -- 成功
-1 -- 失败,设置错误号
int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);
功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
参数:
sockfd -- 用于监听的文件描述符
addr -- 传出参数,记录了连接成功后客户端的地址信息(ip,port)
addrlen -- 制定第二个参数的对应的内存大小
返回值:
成功 -- 返回用于通信的文件描述符
失败 -- (-1)
int connect(int fd, const struct sockaddr, socklen_t addrlen);
功能:客户端连接服务器
参数:
fd -- 用于通信的文件描述符
*buf -- 客户端要连接的服务器的地址信息
addrlen -- 第二个参数的内存大小
返回值:
0 -- 成功
-1 -- 失败
ssize_t write(int fd, const void *buf, size_t count);
// 写数据
ssize_t read(int fd, void *buf, size_t count);
// 读数据
4.15 案例1/2:TCP通信实现(服务器端)
实现套接字通信,需要有两个程序(服务器,客户端)
serve.c
/*
实现TCP通信的服务器端
*/
#include <stdio.h>
#include <arpa/inet>
#include <unistd.h> // read()的头文件
#include <string.h>
#include <stdlib.h>
int main() {
// 1. 创建socket套接字(用于监听)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 每次调用系统API之后要进行判断
if(lfd == -1) {
perror("socket");
exit(-1);
}
// 2. 绑定
struct sockaddr_in saddr;
// 初始化
saddr.sin_family = PF_INET; // 协议族
// inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
/*或*/
saddr.sin_addr.s_ddr = 0; // 相当于0.0.0.0,表示任何地址
saddr.sin_port = htons(9999); // 监听端口
int ret = bind(lfd, (struct sockaddr *)&addr, 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;
it len = sizeof(clientaddr);
int cfd = accept(ldf, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端的信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_adde, 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(len > 0) {
printf("recv client data : %s\n, recvBuf");
} else if(len = 0) {
// 表示客户端断开连接
printf("client closed...\n");
break;
}
char * data = "hello, i am server";
// 给客户端发送数据
write(cfd, data, strlen(data));
}
// 关闭文件描述符
close(cfd);
close(lfd);
return 0;
}
4.16 案例2/2:TCP通信实现(客户端)
client.c
/*
实现TCP通信的客户端
*/
#include <stdio.h>
#include <arpa/inet>
#include <unistd.h> // read()的头文件
#include <string.h>
#include <stdlib.h>
int mian() {
// 1. 创建套接字
int fd = socket(AF_INET, SOCK_STRAM, 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("connet");
exit(-1);
}
// 3. 通信
char recvBuf[1024] = {0};
while(1){
char * data = "hello, i am client";
// 给客户端发送数据
write(fd, data, strlen(data));
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server data : %s\n, recvBuf");
} else if(len = 0) {
// 表示服务器端断开连接
printf("server closed...\n");
break;
}
}
// 关闭连接
close(fd);
return 0;
}
4.17 TCP三次握手
4.17.1 简介
TCP 是一种
三次握手的目的是保证双方互相之间建立了连接。
三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。
4.17.2 TCP结构
4.17.3 TCP三次握手的过程
// 在收到SYN和FIN的时候,会+1。其它的都是+数据
第一次握手:
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(按一个字节算)
4.18 滑动窗口
4.19 TCP四次挥手
4.20 多进程实现并发服务器(1)-- 服务器端
4.20.1 TCP 通信并发
要实现 TCP 通信服务器处理并发的任务,使用多线程或者多进程来解决。
思路:
1. 一个父进程,多个子进程
2. 父进程负责等待并接受客户端的连接
3. 子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信
4.20.2 实现代码
serve_process.c
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
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);
}
// 不断循环,等待客户端连接
while(1) {
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
// 接受连接
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
if(cfd == -1) {
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, port is %d\n", cliIp, cliPort);
// 通信
char recvBuf[1024] = {0};
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 {
// 客户端已经断开连接了
printf("client closed...\n");
}
write(cfd, recvBuf, strlen(recBuf) + 1);
}
close(cfd);
exit(0); // 退出当前子进程
}
}
close(lfd);
return 0;
}
4.21 多进程实现并发服务器(2)-- 客户端
4.21.1 实现代码
client.c
#include <stdio.h>
#include <arpa/inet>
#include <unistd.h> // read()的头文件
#include <string.h>
#include <stdlib.h>
int mian() {
// 1. 创建套接字
int fd = socket(AF_INET, SOCK_STRAM, 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("connet");
exit(-1);
}
// 3. 通信
char recvBuf[1024] = {0};
int i = 0;
while(1){
sprintf(recvBuf, "data: %d\n", i++);
// 给服务端发送数据
write(fd, recvBuf, strlen(recvBuf) + 1);
sleep(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...\n");
break;
}
}
// 关闭连接
close(fd);
return 0;
}
4.21.2 问题点
--> 在 sprintf(recvBuf, "data: %d\n", i++); 中的 “/n” 是没有被recvBuf接收的:此处改为 strlen(recvBuf) + 1 即可。
--> 进程结束之后资源回收问题:
通过wait或者waitpid回收
4.21.3 终极代码
服务端
#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) { waitpid(-1, NULL, WNOHANG); if(ret == -1) { // 所有子进程都回收完了 break; }else if(ret == 0) { // 还有子进程活着 break; }else (ret > 0){ // 被回收的子进程的pid printf("子进程 %d 被回收了\n", ret); } } } int main() { struct sigaction act; act.sa_flags = 0; sigempty(&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, sizeof(cliaddr)); /* if(cfd == -1) { perror("accept"); // 会产生软中断,回来之后 不会阻塞。需要更改 exit(-1); } */ // 更改!!!!!! 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, port is %d\n", cliIp, cliPort); // 通信 char recvBuf[1024] = {0}; 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(recBuf) + 1); } close(cfd); exit(0); // 退出当前子进程 } } close(lfd); return 0; }
客户端
#include <stdio.h> #include <arpa/inet> #include <unistd.h> // read()的头文件 #include <string.h> #include <stdlib.h> int mian() { // 1. 创建套接字 int fd = socket(AF_INET, SOCK_STRAM, 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("connet"); exit(-1); } // 3. 通信 char recvBuf[1024] = {0}; int i = 0; while(1){ sprintf(recvBuf, "data: %d\n", i++); // 给服务端发送数据 write(fd, recvBuf, strlen(recvBuf) + 1); sleep(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...\n"); break; } } // 关闭连接 close(fd); return 0; }
4.22 多线程实现并发服务器
使用多线程的方式实现并发通信
4.23 TCP状态转换
4.23.1 图解
发生在三次握手/四次挥手的过程中,每一端都会有状态的改变;
4.23.2 2MSL(Maximum Segment Lifetime)最大报文生存时间
主动断开连接的一方,最后进出一个TIME_WAIT状态,这个状态会持续:2msl
-- msl: 官方建议为2分钟,实际上是30秒
4.24 半关闭、端口复用
4.24.1 半关闭
#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。
使用close终止一个连接,但它只是减少描述符的引用计数,并不是直接关闭连接,只有当引用计数为0时才关闭连接。shutdown不考虑描述符的引用计数,直接关闭描述符。也可以选择终止一个方向的连接,只中止或只中止写。
注意:
1. 如果有多个进程共享一个套接字,close每调用一次,计数减1,直到计数为0(也就是所有进程都调用了close)套接字将被释放;
2. 在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但如果一个进程close(fd)将不会影响到其它进程。
4.24.2 端口复用
端口复用最常见的用途是:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
#include <sys/type.h>
#include <sys/socket.h>
// 设置套接字属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optal, socklen_t optlen);
参数:
sockfd -- 要操作的文件描述符
level -- 级别 - SOL_SOCKET(端口复用的级别)
optname -- 选项的名称
- SO_REUSEADDR
- SO_REUSEPORT
optval -- 端口复用的值(整形)
- 1 : 可以复用
- 0 : 不能复用
optlen -- optval 参数的大小
端口复用设置的时机是再服务器绑定端口之前:
setsockopt();
bind();
// 常看网络相关信息的命令
netstat
参数:
-a -- 所有的socket
-p -- 显示正在使用socket程序的名称
-n -- 直接使用IP地址而不通过域名服务器
代码:
#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(){
// 创建socket
int lfd = socket(PF_INET, SOCK_STRAM, 0);
if(lfd == -1){
perror("socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.a_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
int optval = 1; // 表示要端口复用
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(saddr));
if(ret == -1){
perror("bind");
return -1;
}
// 监听
int ret = listen(lfd, 8);
if(ret == -1){
perror("listen");
return -1;
}
// 接收客户端连接
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
if(cfd == -1){
perror("accept");
return -1;
}
// 获取客户端信息
char cliIp[16];
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
unsigned short cliPort = ntos(cliaddr.sin_port);
// 输出客户端信息
printf("client's ip is %s, and port is %d\n", cliIp, cliPort);
// 接收客户端发来的数据
char recvBuf[1024] = {0};
while(1){
int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
if(len == -1){
perror("recv");
return -1;
} else if(len == 0){
printf("客户端已断开连接");
break;
} else if(len > 0){
printf("read buf = %s\n", recvBuf);
}
}
// 小写转大写
for(int i = 0; i < len; ++i){
recvBuf[i] = toupper(recvBuf[i]);
}
printf("after buf = %s\n", recvBuf);
// 大写字符发送给客户端
ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
if(ret == -1){
perror("send");
return -1;
}
}
close(cfd);
close(lfd);
return 0;
4.25 IO多路复用简介(I/O多路转接)
I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能。Linux 下实现多路复用的系统调用主要有 select 、poll 和 epoll。
4.25.1 其他模型



4.25.2 IO多路转接技术select/poll/epoll
1. select/poll
select代收员比较懒,他只会告诉你有几个快递到了,但是哪个快递需要挨个遍历一遍
2. epoll
epoll代收快递员很勤快,她不仅会告诉你有几个快递到了,还会告诉你是哪个快递公司的快递
4.26 select API介绍
4.26.1 select介绍
主旨思想:
1. 构造一个关于文件描述符的列表,将要监听的文件描述符添加在列表中;
2. 调用一个系统函数select,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回;
a. 这个函数是阻塞的
b. 函数对文件描述符的检测的操作是由内核完成的
3. 返回时会告诉进程有多少(哪些)描述符进行I/O操作。
// sizeof(fe_set) = 128 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 -- 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
- 一般检测读操作
- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
- 是一个传入传出参数
writefds -- 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
- 委托内核检测写缓冲区是不是还可以写数据(不满的就可以直接写)
exceptfds -- 检测发生异常的文件描述符集合(一般不会用到)
timeout -- 设置的超时时间
struct timeval { long tv_sec; /* second */ long tv_usec; /* microsecond */ }; /* - NULL : 永久阻塞,直到检测到了文件描述符有变化 - tv_sec = 0 && tv_usec = 0 : 不阻塞 - tv_sec > 0 && tv_usec > 0 : 阻塞对应的时间 */
返回值:
-1 -- 失败
>0 (如,n) -- 检测的集合中有n个描述符发生了变化
// 将参数文件描述符对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1,返回值是fd对应的标志位的值
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);
4.26.2 select()工作过程分析
4.27 select代码编写
4.27.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);
// 创建一个fd_set的集合,存放的是需要检测的文件描述符
fd_set rdset, tmp;
FD_ZERO(&rdset); //将标志位置为0
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1) {
tmp = rdset;
// 调用select系统函数,让内核帮检测哪些文件描述符有数据
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);
// 将新的文件描述符加入到集合中
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;
}
4.27.2 客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h> // read()的头文件
#include <string.h>
#include <stdlib.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, "127.0.0.1", &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);
}
close(fd);
return 0;
}
4.28 poll API介绍及代码编写
4.28.1 select()的缺点
1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
2. 同时每次调用select都需要在内核便利传递进来的所有fd,这个开销也很大;
3. select支持的文件描述符太小了,默认是1024;
4. fds集合不能重用,每次都要重置。
4.28.2 poll
#include <poll.h>
struct pollfd {
int fd; // 委托内核检测的文件描述符
short events; // 委托内核检测文件描述符的什么事件
short revents; // 文件描述符实际发生的事件
};
int poll(struct pollfd *fd, nfds_t nfds, int timeout);
参数:
fds -- 是一个 struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
nfds -- 这个是第一个参数数组中最后一个有效元素的下标 + 1
timeout -- 阻塞时长
0:不阻塞
-1:阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
>0:阻塞时长
返回值:
-1 -- 失败
>0(n) -- 成功,n表示检测到集合中有n个文件描述符发生变化
作为events的宏值:
事件 | 常值 | 作为events的值 | 作为revents的值 | 说明 |
读事件* | POLLIN* | ✔ | ✔ | 普通或优先带数据可读 |
POLLRDNORM | ✔ | ✔ | 普通数据可读 | |
POLLRDBAND | ✔ | ✔ | 优先级带数据可读 | |
POLLPRI | ✔ | ✔ | 高优先级数据可读 | |
写事件 | POLLOUT | ✔ | ✔ | 普通或优先带数据可写 |
POLLWRNORM | ✔ | ✔ | 普通数据可写 | |
POLLWRBAND | ✔ | ✔ | 优先级带数据可写 | |
错误事件 | POLLERR | ✔ | 发生错误 | |
POLLHUP | ✔ | 发生挂起 | ||
POLLNVAL | ✔ | 描述不是打开的文件 |
如,定义一个结构体,同时检测读写:
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;
4.28.3 使用poll实现服务器的程序
服务端
#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]; // 表示最多支持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);
// 将新的文件描述符加入到集合中
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(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;
}
4.29 epoll API 介绍
4.29.1 poll()的缺点
1. 每次要将文件描述符集合fds从用户态拷贝到内核态,再从内核态拷贝到用户态。是有一定开销的;
2. 每次在内核中去遍历需要遍历所有的文件描述符的集合。有一定开销;
4.29.2 epoll介绍
#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;
} 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_events, int maxevents, int timeout);
参数:
epfd -- epoll实例对应的文件描述符
events -- 传出参数,保存了发生了变化的文件描述符的信息
maxevents -- 第二个参数结构体数组的大小
tomeout -- 阻塞时间
0:不阻塞
-1:阻塞,直到检测到fd数据发生变化,解除阻塞
>0:阻塞时长(毫秒)
返回值:
成功 -- 返回发生变化的文件描述符个数 >0
失败 -- 返回-1
epoll()多路复用
4.30 epoll 代码编写
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.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实例中
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(2) {
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 {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信(读事件)
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;
}
4.31 epoll的两种工作模式
4.31.1 epoll的两种工作模式
LT模式(水平触发)
假设委托内核检测读事件 --> 检测fd的读缓冲区
读缓冲区有数据 --> epoll 检测到了会给用户通知
a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
b.用户只读了一部分数据,epoll会通知
c.缓冲区数据读完了,不通知
LT(level - triggered)是缺省(不指定时默认)的工作模式,并同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式(边沿触发)
假设委托内核检测读事件 --> 检测fd的读缓冲区
读缓冲区有数据 --> epoll 检测到了会给用户通知
a.用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不通知了
b.用户只读了一部分数据,epoll不通知
c.缓冲区的数据读完了,不通知
ET(edge- triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是需要注意的是,如果一直不对这个 fd 做 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式再很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用被阻塞套接口。以避免由于一个文件句柄(文件描述符)的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
4.31.2 代码实现
4.32 UDP通信实现
4.32.1 API介绍
#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 -- 地址的内存大小
返回值:
-1 -- 失败,设置错误号
>0 -- 发送的字节数量
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 -- 地址的内存大小
返回值:
>0 -- 发送的字节个数
-1 -- 失败
4.32.2 代码实现
4.33 广播
4.33.1 概念
向子网中多台计算机发送消息,并且子网中的所有计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP地址中子网内主机标志部分二进制全部为1。
a.只能在局域网中使用
b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。
设置广播属性的函数
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_optlen);
sockfd -- 文件描述符
level -- 级别,SOL_SOKET
optname -- SO_BROADCAST
optval -- int类型的值,为1表示允许广播
optlen -- optval的大小
4.33.2 代码实现
4.34 组播(多播)
4.34.1 概念
单播地址标识单个IP接口,广播地址标识某个子网的所有IP接口,多播地址标识一组IP接口。单播和广播是寻址方案的两个极端(要么单个要么全部),多播则在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,而多播则既可以用于局域网,也可以跨广域网使用。
a.组播既可以用于局域网,也可以用于广域网
b,客户端需要加入多播组,才能接收到多播的数据
组播地址:IP多播通信必须依赖于IP多播地址,在IPv4中它的范围从 244.0.0.0 到 239.255.255.255,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址3类:
设置组播
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 mreqn
struct ip_mreqn{
// 组播组的IP地址
struct in_addr imr_multiaddr;
// 本地某一网络设备接口的IP地址
struct in_addr imr_address;
int imr_ifindex; // 网卡编号
};
typedef uint32_t in_addr_t;
struct in_addr{
in_addr_t s_addr;
}
4.35 本地套接字
本地套接字的作用:本地的进程间通信
有关系的进程间通信
没有关系的进程间通信
本地套接字实现流程与网络套接字类似,一般采用TCP的流程
本地套接字通信的流程
服务器端流程:
1. 创建监听套接字
int lfd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字要绑定本地的套接字文件 --> server 端
struct sockaddr_un addr;
// 绑定成功后,指定的sum_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
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();