参考连接:https://www.nowcoder.com/study/live/504/2/16。
【Linux】网络编程一:网络结构模式、MAC/IP/端口、网络模型、协议及网络通信过程简单介绍
【Linux】网络编程二:socket简介、字节序、socket地址及地址转换API
【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写
TCP和UDP都是传输层的协议,是传输层比较常用的协议。
UDP | TCP | |
---|---|---|
是否创建链接 | 无连接 | 有连接 |
是否可靠 | 不可靠 | 可靠 |
连接对象个数 | 支持一对一、一对多、多对一、多对多 | 支持一对一 |
传输方式 | 面向数据报 | 基于字节流 |
首部开销 | 一般是8字节 | 最少20字节 |
适用场景 | 实时性要求比较高的场所,如QQ聊天、电话/视频会议、直播等 | 可靠性要求比较高的,比如文件传输 |
TCP通信流程:
#include
#include
#include
int socket(int domain, int type, int protocol);
int bind(int sockfd, cosnt struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
size_t write(int fd, const void *buf, size_t count); // 写数据
size_t read(int fd, void *buf,size_t count); // 读数据
socket()
函数介绍:
socket() |
说明 |
---|---|
函数功能 | 创建一个套接字 |
函数声明 | int socket(int domain, int type, int protocol); |
参数domain |
协议族 - AF_UNIX/AF_LOCAL :本地套接字通信,进程间通信- AF_INET :ipv4- AF_INET6 :ipv6 |
参数type |
通信过程中使用的协议类型 - SOCK_STREAM :流式协议- SOCK_DGRAM :报式协议 |
参数protocol |
具体的协议,设置为0时 如果type=SOCK_STREAM,默认使用TCP; 如果type=SOCK_DGRAM,默认使用UDP |
返回值 | 成功返回socket的文件描述符 失败返回-1 |
bind()
函数介绍:
bind() |
说明 |
---|---|
函数功能 | 绑定,将socket的文件描述符和本地 IP+Port进行绑定;有时也称socket命名 |
函数声明 | int bind(int sockfd, cosnt struct sockaddr *addr, socklen_t addrlen); |
参数sockfd |
socket的文件描述符 |
参数addr |
需要绑定的socket地址,封装了IP和port的信息,类型为sockaddr |
参数len |
参数addr 结构体栈的内存大小 |
返回值 | 成功返回0 失败返回-1 |
listen()
函数介绍:
listen() |
说明 |
---|---|
函数功能 | 监听服务器socket是连接 |
函数声明 | int listen(int sockfd, int backlog); |
参数sockfd |
socket的文件描述符 |
参数backlog |
未连接的和已经连接的socket的和的最大值,可以通过文件/proc/sys/net/core/somaxconn 查看 |
返回值 | 成功返回0 失败返回-1 |
accept()
函数介绍:
accept() |
说明 |
---|---|
函数功能 | 接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 |
函数声明 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
参数sockfd |
用于监听的socket文件描述符 |
参数addr |
记录了连接成功后客户端的地址信息,IP和端口号 |
参数addrlen |
指定参数addr 对应的内存大小,是一个指针 |
返回值 | 成功返回用于通信的文件描述符 失败返回-1 |
connect()
函数介绍:
connect() |
说明 |
---|---|
函数功能 | 客户端连接服务器 |
函数声明 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
参数sockfd |
用于通信的socket文件描述符 |
参数addr |
客户端要连接的服务器的地址信息 |
参数addrlen |
指定参数addr 对应的内存大小 |
返回值 | 成功返回0 失败返回-1 |
服务器端server.c
:
/**
* @file server.c
* @author zoya ([email protected])
* @brief 实现TCP服务器端的通信
* @version 0.1
* @@date: 2022-10-09
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
#include
int main()
{
int ret = -1;
// 创建socket,用于监听
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
exit(-1);
}
// 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
#if 1
ret = inet_pton(AF_INET, "192.168.109.130", &addr.sin_addr.s_addr); // 字符串转换为整型IP地址
if (ret != 1)
{
perror("inet_pton");
exit(-1);
}
#else
addr.sin_addr.s_addr = INADDR_ANY; // 表示0.0.0.0,表示无线和网卡都绑定
#endif
addr.sin_port = htons(9999); // 主机字节序转换为网络字节序
ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
// 监听连接
if (-1 == listen(sockfd, 5))
{
perror("listen");
exit(-1);
}
// 接收客户端连接,阻塞
struct sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr);
int clientSocket_fd = accept(sockfd, (struct sockaddr *)&clientAddr, &len);
if (clientSocket_fd == -1)
{
perror("accept");
exit(-1);
}
// 输出客户端信息
char clientIP[16];
inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short cPort = ntohs(clientAddr.sin_port);
printf("client ip : %s, port : %d\n", clientIP, cPort);
// 通信
// 获取客户端数据,给客户端发送确认信息
char recvbuf[1024] = {0};
while (1)
{
ssize_t size = read(clientSocket_fd, recvbuf, sizeof(recvbuf));
if (size == -1)
{
perror("read");
exit(-1);
}
else if (size > 0)
{
printf("server receive client buf %ld: %s\n", size, recvbuf);
}
else if (size == 0)
{
// 读到的字节为0表示客户端断开连接
printf("client closed...");
break;
}
char *str = "hello, i am server!";
size = write(clientSocket_fd, str, strlen(str));
}
// 关闭文件描述符
close(clientSocket_fd);
close(sockfd);
return 0;
}
客户端client.c
:
/**
* @file client.c
* @author zoya ([email protected])
* @brief tcp通信 客户端
* @version 0.1
* @@date: 2022-10-09
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
int main()
{
int ret = -1;
// 创建套接字
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
{
perror("socket");
exit(-1);
}
// 连接服务器
struct sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.109.130", &servAddr.sin_addr.s_addr);
servAddr.sin_port = htons(9999);
ret = connect(cfd, (struct sockaddr *)&servAddr, sizeof(servAddr));
if (ret == -1)
{
perror("connect");
exit(-1);
}
// 通信
// 发送数据
char recvBuf[1024] = {0};
while (1)
{
char *str = "hello,i'm client!";
ssize_t size = write(cfd, str, strlen(str));
size = read(cfd, recvBuf, sizeof(recvBuf));
if (size == -1)
{
perror("read");
exit(-1);
}
else if (size > 0)
{
printf("client receive server buf : %s\n", recvBuf);
}
else if (size == 0)
{
printf("与服务器断开连接");
break;
}
sleep(1);
}
// 关闭连接
close(cfd);
return 0;
}
TCP是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的”连接“,其实是客户端和服务器的内存里保存的一份关于对方的信息,如IP地址、端口号等。
TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数,这些参数可以放在TCP头部。
TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手
建立一个连接,采用四次挥手
来关闭一个连接。
三次握手的目的是保证双方互相之间建立了连接。
三次握手发生在客户端请求连接时,调用
connect()
是,底层会进行三次握手。三次握手保障了客户端和服务器互相了解自己及对方的收、发信息没有问题。
port number
:告知主机报文段来自哪里(源端口号)以及给哪个上层协议或应用程序(目的端口)。进行TCP通信时,客户端通常使用系统自动选择的临时端口号。sequence number
:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系统初始化位某个随机值ISN
(Initial Sequence Number
,初始序号值)。那么在该传输方向上(从A到B),后续的TCP报文段中序号值将被系统设置成ISN
加上 该报文段所携带数据的第一个字节在整个字节流中的偏移。
acknowledgement number
:用作对另一方发送来的TCP报文段的响应,其值是收到的TCP报文段的序号值 + 标志位长度(SYN/FIN) + 数据长度。
head length
:标识TCP头部有多少个32bit(4字节)。URG
:标识紧急指针(urgent pointer
)是否有效;ACK
:标识确认好是否生效,称携带ACK标志的TCP报文段为确认报文段。PSH
:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区)。RST
:表示要求对方重新建立连接,称携带RST标志的TCP报文段为复位报文段。SYN
:表示请求建立一个连接,称携带SYN标志的TCP报文段为同步报文段。FIN
:表示通知对方本端要关闭连接了,称携带FIN标志的TCP报文段为结束报文段。window size
:是TCP流量控制的一个手段,这里说的窗口,指的是接收通告窗口(Receiver Window
,RWND
)。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。TCP checksum
:由发送端填充,接收端对TCP报文段执行CRC算法以校验TCP报文段在传输过程中是否损坏。需要注意,这个校验不仅包括TCP头部,也包括数据部分,这也是TCP可靠传输的一个重要保障。urgent pointer
:是一个整型的偏移量,和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。确切的说,这个字段是紧急指针相对当前序号的偏移,可以称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。第一次握手
第二次握手
第三次握手
问题1:如何确定发送的数据是完整的?
问题2:如何确定接收数据的顺序和发送数据的顺序是一直的?
通过序号和确认序号,可以确定发送的数据是完整的,也可以确定接收数据的顺序和发送数据的顺序是一致的。
滑动窗口,Sliding window
,是一种流量控制技术,早期的网络通信中,通信双方不会考虑网络的拥挤情况,直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发送不了数据,所以有了滑动窗口机制来解决此问题。
滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包,称为窗口尺寸。
TCP中采用使用滑动窗口进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方一般不能再发送数据报。
滑动窗口是TCP中实现诸如ACK确认、流量控制、拥塞控制的承载结构。
窗口可以简单理解为缓冲区的大小。滑动窗口的大小是随着发送数据和接收数据变化的,每一端通信的双方都有发送缓冲区和接收数据的缓冲区。对服务器来说,有发送缓冲区和接收缓冲区;对客户端来说也有发送缓冲区和接收缓冲区;那么对应的,服务器端和客户端都有发送缓冲区的窗口和接收缓冲区的窗口。
四次挥手发生在断开连接时,程序中调用close()
会使用TCP协议进行四次挥手。
客户端和服务端都可以主动发起断开连接,谁先调用close()
,谁就是发起。
在TCP连接时,采用三次握手建立的连接是双向的,在断开的时候也需要双向断开连接。
如下:
要实现TCP通信服务器处理并发任务,可以使用多线程或者多进程解决。
解决思路1:多进程解决
示例代码:
client.c
:
/**
* @file 1client.c
* @author zoya([email protected])
* @brief TCP通信客户端,循环向服务器发送消息,并接收服务器返回的消息
* @version 0.1
* @date 2022-10-10
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
int main()
{
int ret = -1;
// 创建socket
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
{
perror("[client] : socket");
exit(-1);
}
// 请求连接
uint16_t g_port = 9999;
char *g_ip = "192.168.57.128";
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(g_port); // 格式转换,主机字节序port转换为网络字节序port
ret = inet_pton(AF_INET, g_ip, &addr.sin_addr.s_addr); // 格式转换,主机字节序ip转换为网络字节序ip
if (ret == 0)
{
printf("[client] : string ip is not a valid network address.\n");
exit(-1);
}
else if (ret == -1)
{
perror("[client] : inet_pton");
exit(-1);
}
ret = connect(cfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("[client] : connect");
exit(-1);
}
// 发送数据
char recvBuf[1024] = {0};
char sendBuf[1024] = {0};
int num = 0;
while (1)
{
// 每隔1s发送、接收消息
memset(recvBuf, 0, sizeof(recvBuf));
memset(sendBuf, 0, sizeof(sendBuf));
sprintf(sendBuf, "hello,i am client, this is %dth message.", ++num);
ssize_t size = write(cfd, sendBuf, strlen(sendBuf));
// 接收数据
size = read(cfd, recvBuf, sizeof(recvBuf));
if (size > 0)
{
printf("[client] : receive buf - %s\n", recvBuf);
}
else if (size == -1)
{
perror("[client] : read");
break;
}
else if (size == 0)
{
printf("[client] : disconnect!\n");
break;
}
sleep(1);
}
// 关闭文件描述符
close(cfd);
return 0;
}
server.c
:
/**
* @file 1server.c
* @author zoya([email protected])
* @brief 服务器端实现并发处理,进程实现
* @version 0.1
* @date 2022-10-13
*
* @copyright Copyright (c) 2022
*
*/
#define _XOPEN_SOURCE 500
#include
#include
#include
#include
#include
#include
#include
#include
#include
void recyChild(int signum)
{
while (1)
{
int ret = waitpid(-1, NULL, WNOHANG); // 设置为非阻塞,-1表示回收所有的子进程
if (ret == -1)
{
// 所有的子进程都回收了
break;
}
else if (ret == 0)
{
// 还有子进程 活着
break;
}
else if (ret > 0)
{
// 还有子进程没有被回收
printf("子进程 %d 被回收了\n", ret);
}
}
}
int main()
{
// 捕捉信号 SIGCHLD
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyChild;
sigaction(SIGCHLD, &act, NULL);
// 创建SOCKET
int lfd = socket(AF_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 = -1;
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 caddr;
int len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); // 返回客户端的文件描述符
if (cfd == -1)
{
// EINTR : 软中断,在连接到达之前,如果有信号则调用会被信号中断
if (errno == EINTR)
{
// 说明产生了中断
continue;
}
perror("accept");
exit(-1);
}
// 每一个连接,就创建一个子进程,与客户端通信
pid_t pid = fork();
if (pid == 0)
{
// 子进程 进行通信
//获取客户端信息
char cip[16];
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, cip, sizeof(cip));
unsigned short cport = ntohs(caddr.sin_port);
printf("child process : %d client ip : %s, port : %d\n", getpid(), cip, cport);
// 接受客户端发送的数据
char recvbuf[1024] = {0};
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
ssize_t size = read(cfd, recvbuf, sizeof(recvbuf));
if (size == -1)
{
perror("read");
break;
}
else if (size > 0)
{
printf("child process recv : %s\n", recvbuf);
}
else if (size == 0)
{
printf("child process, client disconnect...\n");
break;
}
// 发送数据给客户端
write(cfd, recvbuf, strlen(recvbuf));
}
close(cfd);
exit(0);
}
}
close(lfd);
return 0;
}
解决思路2:多线程解决
server,c
:
/**
* @file 1serve_thread.c
* @author zoya([email protected])
* @brief 多线程实现并发服务器
* @version 0.1
* @date 2022-10-13
*
* @copyright Copyright (c) 2022
*
* 没有一个连接就创建一个线程,在线程中接受或发送数据
* 主线程连接通信
*
*/
#include
#include
#include
#include
#include
#include
#include
struct sockInfo
{
int fd; // 文件描述符
pthread_t tid; // 线程号
struct sockaddr_in addr; // 客户端的地址信息
};
struct sockInfo g_sockinfos[128]; // 同时支持128个客户端连接
void *callback(void *arg)
{
// 子线程和客户端通信 需要的信息可能有 : 客户端的文件描述符cfd,客户端的地址信息,线程号
struct sockInfo *sockinfo = (struct sockInfo *)arg;
int cfd = sockinfo->fd;
//获取客户端信息
char cip[16];
inet_ntop(AF_INET, &sockinfo->addr.sin_addr.s_addr, cip, sizeof(cip));
unsigned short cport = ntohs(sockinfo->addr.sin_port);
printf("client ip : %s, port : %d\n", cip, cport);
char recvbuf[1024] = {0};
while (1)
{
// 接收数据
ssize_t size = read(cfd, recvbuf, sizeof(recvbuf));
if (size > 0)
{
printf("recv msg : %s\n", recvbuf);
}
else if (size == -1)
{
perror("read");
break;
}
else if (size == 0)
{
printf("client disconnect...\n");
break;
}
write(cfd, recvbuf, strlen(recvbuf));
}
close(cfd);
return NULL;
}
int main()
{
// 创建SOCKET
int lfd = socket(AF_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 = -1;
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(g_sockinfos) / sizeof(g_sockinfos[0]);
for (int i = 0; i < max; i++)
{
bzero(&g_sockinfos[i], sizeof(g_sockinfos[i]));
g_sockinfos[i].fd = -1; //
g_sockinfos[i].tid = -1; //
}
// 不断循环等待连接,有连接,创建子线程
while (1)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); // 返回客户端的文件描述符
if (cfd == -1)
{
perror("accept");
exit(-1);
}
// 每一个连接,就创建一个子线程
struct sockInfo *sockinfo;
for (int i = 0; i < max; i++)
{
// 从数组中找到可用的元素
if (g_sockinfos[i].fd == -1)
{
sockinfo = &g_sockinfos[i];
break;
}
if (i == max - 1)
{
sleep(1);
i--;
}
}
sockinfo->fd = cfd;
memcpy(&sockinfo->addr, &caddr, len);
ret = pthread_create(&sockinfo->tid, NULL, callback, sockinfo);
if (ret != 0)
{
printf("[pthread_create]: %s", strerror(ret));
exit(0);
}
pthread_detach(sockinfo->tid); // 设置线程分离
}
close(lfd);
return 0;
}
TCP状态转换发生在三次握手和四次挥手过程中。
三次握手:
SYN_SENT
状态;LISTEN
状态,收到客户端的连接请求,变为SYN_RCVD
状态;ESTABLISHED
状态;ESTABLISHED
状态;四次挥手:
FIN_WAIT_1
;FIN
请求后,服务端变为CLOSE_WAIT
(等待关闭),服务端回复客户端ACK相应;FIN_WAIT_2
;LAST_ACK
;TIME_WAIT
,并向客户端发送ACK;主动断开连接的一方,最后进入一个
TIME_WAIT
状态,这个状态持续的时间是:2MSL(Maximum Segement Lifetime
)。官方建议msl是2分钟(ubuntu中实际测试是30s)。MAL是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
TIME_WAIT
状态也称为2MSL状态。当一端主动发起关闭,发出最后一个ACK后,即第三次挥手完成后发送了第四次挥手的ACK后就进入TTIME_WAIT
状态,必须在此状态上停留两倍的MSL时间,主要目的是怕最后一个ACK包对方没有收到,那么对方在超时后将重发第三次挥手的FIN包,主动关闭端接到重发的FIN后再重发ACK应答。再
TIME_WAIT
状态下,两端的端口不能使用,要等到2MSL时间结束才可以继续使用。当连接处于2MSL等待阶段时,任何迟到的报文段都将被丢弃。参考:什么是2MSL。
当TCP连接主动关闭方接收到被动关闭方发送的FIN和最终的ACK后,主动关闭方必须处于
TIME_WAIT
状态并持续2MSL时间。这样做的目的是能够让TCP连接的主动关闭方在它发送的ACK丢失的情况下重新发送最终的ACK。
主动关闭方重新发送的最终ACK是因为被动关闭方重传了它的FIN,被动关闭方总是重传FIN直到它收到一个最终的ACK。
当TCP连接中A向B发送FIN请求关闭,另一端B回应ACK之后(A进入
FIN_WAIT_2
状态),并没有立即发送FIN给A,A处于半连接状态(半开关),此时A可以接收B发送的数据,但是A不能向B发送数据。
可以使用相应的API控制实现半连接状态。
#include
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不考虑文件描述符的引用计数,直接关闭文件描述符。也可以选择终止一个方向的连接,只终止读或只终止写。
如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,所有进程都调用close,套接字被释放。
在多进程中如果一个进程调用了shurdown(sfd,SHUT_RDWR),其它进程将无法进行通信,但如果一个进程close(sfd)将不会影响其它进程。
端口复用最常用的用途:
#include
#include
// 设置端口复用,也可以设置端口状态
int setsockopt(int sockfd, int level, int optname, consr void *optval, socklen_t *optlen); // 该函数仅用于套接字
sockfd
:指向一个打开的套接字描述符;level
:级别,使用SOL_SOCKET(端口复用的级别);optname
:选项名称,端口复用使用以下:
SO_RUSEADDR
SO_RUSEPORT
optval
:端口复用的值,整型;
- 1 表示可以复用;
- 0 表示不可复用;
optlen
:optval参数的大小;
端口复用设置的时机在服务器绑定端口之前。
socket() // 创建socket
setsockopt() // 设置端口复用
bind() // 绑定
端口复用示例:
server.c
中设置端口复用:
/**
* @file 2server.c
* @author zoya([email protected])
* @brief 接收客户端的消息,并转换消息
* @version 0.1
* @date 2022-10-14
*
* @copyright Copyright (c) 2022
*
*/
#define _XOPEN_SOURCE 500
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void my_handler(int signum)
{
while (1)
{
int ret = waitpid(-1, NULL, WNOHANG); // 设置为非阻塞回收资源
if (ret > 0)
{
printf("[server]: 子进程 %d 被回收了\n", ret);
}
else if (ret == -1)
{
// 所有的子进程都被回收了
break;
}
else if (ret == 0)
{
// 还有子进程没有被回收,说明还有子进程需要执行,暂时不需要回收
break;
}
}
}
int main()
{
// 注册信号SIGCHLD处理函数,回收子进程资源
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = my_handler;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 创建socket
int sfd = socket(PF_INET, SOCK_STREAM, 0);
if (sfd == -1)
{
perror("[server] : socket()");
exit(-1);
}
int ret = -1;
// 设置I/O复用
int optval = 1;
ret = setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
if (ret == -1)
{
perror("[server] : setsockopt()");
exit(-1);
}
// 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("[server] : bind()");
exit(-1);
}
// 监听
ret = listen(sfd, 128);
if (ret == -1)
{
perror("[server] : listen()");
exit(-1);
}
while (1)
{
// 接收连接
struct sockaddr_in clieaddr;
socklen_t len = sizeof(clieaddr);
int cfd = accept(sfd, (struct sockaddr *)&clieaddr, &len);
if (ret == -1)
{
if (errno == EINTR)
{
continue;
}
perror("[server] : accept()");
break;
}
// 输出客户端信息
char clieip[16] = {0};
inet_ntop(AF_INET, &clieaddr.sin_addr.s_addr, clieip, sizeof(clieip));
int clieport = ntohs(clieaddr.sin_port);
printf("[server] : client ip : %s, port : %d\n", clieip, clieport);
// 创建子进程与客户端通信
pid_t pid = fork();
if (pid == 0)
{
// 子进程,处理与客户端通信
char recbuf[1024] = {0};
char sendbuf[1024] = {0};
while (1)
{
memset(recbuf, 0, sizeof(recbuf));
memset(sendbuf, 0, sizeof(sendbuf));
//读取客户端消息
ssize_t size = read(cfd, recbuf, sizeof(recbuf));
if (size > 0)
{
printf("[server-%d] : recv msg , %s\n", getpid(), recbuf);
}
else if (size == -1)
{
perror("[server] : read()");
break;
}
else if (size == 0)
{
printf("[server] : client disconnect...\n");
break;
}
/// 向客户端发送消息
for (int i = 0; i < strlen(recbuf); i++)
{
sendbuf[i] = toupper(recbuf[i]);
}
sendbuf[strlen(recbuf)] = '\0';
write(cfd, sendbuf, strlen(sendbuf));
}
close(cfd);
exit(-1);
}
}
close(sfd);
return 0;
}
I/O多路复用有时也称为I/O多路转接。
I/O多路复用使 程序能够同时监听多个文件描述符,能够提高程序的性能。Linux实现I/O多路复用的系统调用主要有select
、poll
、epoll
。
阻塞等待 BIO-Blocking I/O
不占用CPU宝贵的时间片;但是同一时刻只能处理一个操作,效率低;
非阻塞,忙轮询 NIO-Non-Blocking I/O
提高了程序的执行效率;但是需要占用更多的CPU和系统资源,每循环内有O(n)的系统调用;;
IO复用
信号驱动
异步
select
- 构造一个文件描述符列表,将要监听的文件描述符添加到该列表中。
- 调用系统函数
select()
监听该列表中的文件描述符,直到这些文件描述符中的一个或多个进行I/O操作时,该函数才返回。select()
是阻塞的,且对文件描述符的检测的操作是由内核完成的。- 在返回时,该函数会告诉进程有多少文件描述符要进行I/O操作。
#include
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select函数参数介绍:
nfd
:委托内核检测的最大文件描述符的值 +1.
readfds
:要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读属性;
- 检测读数据;
- 对应的是对方发送的数据,检测读缓冲区。
wrfdsite
:要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写属性;
- 委托内核检测写缓冲区是不是还可以写数据(即检测写缓冲区是否满了);
- 一般不检测写缓冲区,设置为NULL;
exceptfds
:检测发生异常的文件描述符的集合,一般不使用设置为NULL。
timeout
:设置的超时时间;
如果为NULL表示永久阻塞,直到检测到文件描述符有变化;
如果
tv_sec
和tv_usec
都为0表示不阻塞;如果
tvsec>0
和tvusec>0
表示阻塞对应的时间;struct timeval{ long tv_sec; long tv_usec; }
select返回值:
- -1:表示失败;
- >0:表示检测的集合中有n个文件描述符发生了变化
如下函数是对二进制位的一些操作:
void FD_CLR(int fd,fd_set *set); // 对fd对应的标志位置为0
int FD_ISSET(int fd, fd_set *set); // 判断fd对应的标志位是0还是1,返回值是fd对应的标志位的值
void FD_SET(int fd, fd_set *set); // 将fd对应的标志位设置为1;
void FD_ZERO(fd_set *set); // 设置文件描述符集set对应的文件描述符的标志位都为0
select
使用示例:
// server.c
/**
* @file 1server_select.c
* @author zoya([email protected])
* @brief TCP通信服务端:select实现
* @version 0.1
* @date 2022-10-14
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 创建socket
int sfd = socket(PF_INET, SOCK_STREAM, 0);
if (sfd == -1)
{
perror("[server] : socket()");
exit(-1);
}
int ret = -1;
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
ret = bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("[server] : bind()");
exit(-1);
}
// 监听
ret = listen(sfd, 128);
if (ret == -1)
{
perror("[server] : listen()");
exit(-1);
}
// NIO模型
// 创建文件描述符集合
fd_set rdset, rdsettmp;
FD_ZERO(&rdset); // 标志位全部置为0
FD_SET(sfd, &rdset);
int maxfd = sfd;
while (1)
{
rdsettmp = rdset;
// 调用select,检测哪些文件描述符有数据
ret = select(maxfd + 1, &rdsettmp, NULL, NULL, NULL); // 一直阻塞直到有文件描述符发生变化
if (ret > 0)
{
// 有文件描述符对应的缓冲区数据发生改变
// 遍历检查是哪个文件描述符发生了改变
if (FD_ISSET(sfd, &rdsettmp))
{
// 有新的客户端连接,接收连接
// 接收连接
struct sockaddr_in clieaddr;
socklen_t len = sizeof(clieaddr);
int cfd = accept(sfd, (struct sockaddr *)&clieaddr, &len);
if (ret == -1)
{
perror("[server] : accept()");
break;
}
// 输出客户端信息
char clieip[16] = {0};
inet_ntop(AF_INET, &clieaddr.sin_addr.s_addr, clieip, sizeof(clieip));
int clieport = ntohs(clieaddr.sin_port);
printf("[server] : client ip : %s, port : %d\n", clieip, clieport);
// 把连接的客户端的文件描述符加入到集合中
FD_SET(cfd, &rdset);
// 更新最大的文件描述符
maxfd = (maxfd > cfd) ? maxfd : cfd;
}
for (int i = sfd + 1; i < maxfd + 1; i++)
{
if (FD_ISSET(i, &rdsettmp))
{
// 判断文件描述符i是不是为1,1说明这个文件描述符对应的客户端发来了数据
char buf[1024] = {0};
int size = read(i, buf, sizeof(buf));
if (size == -1)
{
perror("[server] : read()");
exit(-1);
}
else if (size == 0)
{
// 对方断开连接
printf("[server] : client disconnect...\n");
FD_CLR(i, &rdset);
}
else if (size > 0)
{
printf("[server] : recv msg : %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
else if (ret == -1)
{
perror("[server] : select()");
exit(-1);
}
else if (ret == 0)
{
// 0表示超时时间到了,没有任何文件描述符发生改变
continue;
}
}
}
select
的缺点:
poll
poll
是对select的改进。
#include
struct pollfd{
int fd; // 委托内核检测的文件描述符
short events; // 委托内核检测文件描述符的什么事件
short revents; // 文件描述符实际发生的事情
}
int poll(struct pollfd *fds, nfds_t nfds,int timeout);
参数说明:
fds
:struct pollfd
结构体数组,是一个需要检测的文件描述符的集合;没有个数1024的限制;nfds
:第一个参数中最后一个有效元素的下标 + 1timeout
:阻塞时长,0表示不阻塞;-1表示阻塞,当检测到需要检测的文件描述符有变化,解除阻塞;>0的值表示阻塞的时长,单位:毫秒;返回值:
事件 | 常值 | 作为events的值 | 作为revents的值 | 说明 |
---|---|---|---|---|
读事件 | POLLN | √ | √ | 普通或优先带数据可读 |
读事件 | POLLRDNORM | √ | √ | 普通数据可读 |
读事件 | POLLRDBAND | √ | √ | 优先级带数据可读 |
读事件 | POLLPRI | √ | √ | 高优先级数据可读 |
写事件 | POLLOUT | √ | √ | 普通或优先带数据可写 |
写事件 | POLLWRNORM | √ | √ | 普通数据可写 |
写事件 | POLLWRBAND | √ | √ | 优先级带数据可写 |
错误事件 | POLLERR | √ | 发生错误 | |
错误事件 | POLLHUP | √ | 发生挂起 | |
错误事件 | POLLNVAL | √ | 描述不是打开的文件 |
poll
的缺点:
poll
使用示例:
// server.c
/**
* @file 1poll_server.c
* @author zoya ([email protected])
* @brief TCP通信服务端:poll实现IO多路复用
* @version 0.1
* @date 2022-10-17
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
#include
int main()
{
// 创建socket
int sfd = socket(PF_INET, SOCK_STREAM, 0);
if (sfd == -1)
{
perror("[server] : socket()");
exit(-1);
}
int ret = -1;
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
ret = bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret == -1)
{
perror("[server] : bind()");
exit(-1);
}
// 监听
ret = listen(sfd, 128);
if (ret == -1)
{
perror("[server] : listen()");
exit(-1);
}
// 创建pollfd结构体数组
struct pollfd fds[1024];
// 初始化pollfd结构体数组
for (int i = 0; i < sizeof(fds) / sizeof(fds[0]); i++)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = sfd;
int maxfd = 0;
while (1)
{
// 调用select,检测哪些文件描述符有数据
ret = poll(fds, maxfd + 1, -1); // -1表示阻塞直到有文件描述符发生变化
if (ret > 0)
{
// 有文件描述符发生变化,表示有连接
if (fds[0].revents & POLLIN)
{
// 有新的客户端连接
struct sockaddr_in clieaddr;
socklen_t len = sizeof(clieaddr);
int cfd = accept(sfd, (struct sockaddr *)&clieaddr, &len);
if (ret == -1)
{
perror("[server] : accept()");
break;
}
// 输出客户端信息
char clieip[16] = {0};
inet_ntop(AF_INET, &clieaddr.sin_addr.s_addr, clieip, sizeof(clieip));
int clieport = ntohs(clieaddr.sin_port);
printf("[server] : client ip : %s, port : %d\n", clieip, clieport);
// 把新的客户端连接加入到fds数组中
for (int i = 1; i < 1024; i++)
{
if (fds[i].fd == -1)
{
fds[i].fd = cfd;
fds[i].events = POLLIN;
maxfd = maxfd > i ? maxfd : i;
break;
}
}
}
for (int i = 1; i < maxfd + 1; i++)
{
if (fds[i].revents & POLLIN)
{
// 接收、发送数据
char buf[1024] = {0};
int size = read(fds[i].fd, buf, sizeof(buf));
if (size == -1)
{
perror("[server] : read()");
exit(-1);
}
else if (size == 0)
{
// 对方断开连接
printf("[server] : client disconnect...\n");
fds[i].fd = -1;
fds[i].events = POLLIN;
}
else if (size > 0)
{
printf("[server] : recv msg : %s\n", buf);
write(fds[i].fd, buf, strlen(buf));
}
}
}
}
else if (ret == -1)
{
perror("[server] : select()");
exit(-1);
}
else if (ret == 0)
{
// 0表示超时时间到了,没有任何文件描述符发生改变
continue;
}
}
return 0;
}
epoll
epoll
的原理
int epfd = epoll_create()
在内核中创建epoll实例,类型为struct eventpoll
;返回文件描述符,用于操作内核中的文件描述符。epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&ev)
委托内核检测文件描述符对应的缓冲区是否发生变化;epoll_wait(epfd,...)
告知内核从rbr
中检测是否有文件描述符的信息发生了改变,如果有变化,就把所有信息复制到rdlist
中。#include
struct eventpoll{
...
struct rb_root rbr; // 采用红黑树的数据结构,查找效率比较高
struct list_head rdlist; // 记录需要检测的文件描述符,双链接的形式
...
};
struct union epoll_data{
void *ptr;
int fd; // 常用的是fd
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event{
int events; // 检测哪些事件
epoll_data_t data; // 用户数据信息
};
int epoll_create(int size);
// 创建一个新的epoll实例,在内核中创建了一个数据,这个数据中比较重要的有两个rbr和rdlist,
// rbr表示需要检测的文件描述符的信息(红黑树);
// rdlist存放检测到数据发送改变的文件描述符信息(双链表);
// 参数size:Linux2.6.8之后被忽略,但必须大于0;
// 返回值: 失败返回-1;成功返回文件描述符,通过该返回值可以操作epoll实例。
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op,int fd, struct epoll_event *event);
// 参数
// - epfd:epoll实例对应的文件描述符
// - op:要进行的操作
// - EPOLL_CTL_ADD:添加
// - EPOLL_CTL_MOD:修改
// - RPOLL_CTL_DEL:删除
// - fd:要检测的文件描述符
// - event:检测文件描述符的操作
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 参数:
// - epfd:epoll实例对应的文件描述符;
// - events:传出参数,保存了发生变化的文件描述符信息;
// - maxevents:参数events数组的大小;
// - timeout:阻塞时间,0表示不阻塞;-1表示阻塞直到检测到文件描述符发生变化;>0表示阻塞的时间,单位是ms;
// 返回值:
// - 成功,返回发生变化的文件描述符的个数 >0;
// - 失败,返回-1;
常见的epoll检测事件:
EPOLLIN
:读缓冲区变化;EPOLLOUT
:写缓冲区变化EPOLLERR
:错误;EPOLLET
:设置边沿触发模式;epoll的工作模式有LT模式和ET模式,即水平触发和边沿触发。
- LT模式:
LT,
level-triggered
,水平触发,是缺省的工作方式,同时支持block
和no-block socket
。在这种做法中,内核告诉一个文件描述符是否就绪了,就可以对这个就绪的fd进行IO操作,如果不做任何操作,内核继续通知。假设委托内核检测读事件,即检测fd的读缓冲区
读缓冲区有数据,即
epoll
检测到了给用户通知
- 用户不读数据,数据一直在缓冲区,epoll一直通知
- 用户读一部分数据,epoll仍然通知
- 缓冲区中的数据读完,epoll不通知
ET模式:
ET,
edge-triggered
,边沿触发,是高速工作模式,只支持no-block socket
。这种模式下,当描述符从未就绪变为就绪时,内核通过epoll
通知,它假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd做IO操作,从而导致它再次变成未就绪,内核不会发送更多的通知。ET模式在很大程度上减少了
epoll
事件被重复触发的次数,因此效率比LT模式高。epoll
工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。ET模式下,需要配合
循环读数据和非阻塞的方式读取数据
。假设委托内核检测读事件,->检测fd的读缓冲区
- 读缓冲区有数据,即
epoll
检测到了给用户通知
- 用户不读数据,数据一直在缓冲区,epoll下一次检测不通知
- 用户读一部分数据,
epoll
下一次不会通知- 缓冲区中的数据读完,
epoll
不通知
本地套接字用来实现本地进程间通信(有关系和没有关系的进程间通信)。本地套接字和网络套接字类似,一般采用TCP通信流程。
本地套接字通信流程:
int lfd = socket(AF_UNIX,SOCK_STREAM,0);
struct sockaddr_un addr
,bind(lfd,addr,len);
listen(lfd,128);
int cfd = accept(lfd,caddr,len);
int fd = socket(AF_UNIX,SOCK_STREAM,0);
struct sockaddr_un addr
,bind(lfd,addr,len);
connet(fd,saddr,len);
本地套接字通信示例:
server.c
:
/**
* @file 2server_ipc.c
* @author zoya ([email protected])
* @brief 本地套接字服务端
* @version 0.1
* @@date: 2022-10-18
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
#include
#define PATH_UNIX 100
int main()
{
// 删除服务端sock
unlink("server.sock");
// 创建监听套接字
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(-1);
}
int ret = -1;
// 绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "server.sock"); // 服务端套接字生成的文件
ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
// 监听
ret = listen(lfd, 128);
if (ret == -1)
{
perror("listen");
exit(-1);
}
// 等待客户端连接
struct sockaddr_un caddr;
socklen_t len = sizeof(caddr);
int cfd = accept(lfd, (struct sockaddr *)&caddr, &len);
if (cfd == -1)
{
perror("accept");
exit(-1);
}
printf("client socket filename : %s\n", caddr.sun_path);
// 通信
char buf[128] = {0};
while (1)
{
memset(buf, 0, sizeof(buf));
int size = recv(cfd, buf, sizeof(buf), 0);
if (size == -1)
{
perror("recv");
exit(-1);
}
else if (size == 0)
{
printf("client disconnect...\n");
break;
}
else if (size > 0)
{
printf("client say : %s\n", buf);
// 发送数据
send(cfd, buf, size, 0);
}
}
close(cfd);
close(lfd);
return 0;
}
client.c
:
/**
* @file 2client_ipc.c
* @author zoya ([email protected])
* @brief 本地套接字通信客户端
* @version 0.1
* @@date: 2022-10-18
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
#include
int main()
{
// 删除客户端sock
unlink("client.sock");
// 创建套接字
int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (cfd == -1)
{
perror("socket");
exit(-1);
}
int ret = -1;
// 绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "client.sock"); // 客户端套接字生成的文件
ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
struct sockaddr_un saddr;
saddr.sun_family = AF_UNIX;
strcpy(saddr.sun_path, "server.sock"); // 连接服务端套接字文件
socklen_t len = sizeof(saddr);
// 主动连接服务器
ret = connect(cfd, (struct sockaddr *)&saddr, len);
if (ret == -1)
{
perror("connect");
exit(-1);
}
// 通信
char buf[128] = {0};
int num = 0;
while (1)
{
memset(buf, 0, sizeof(buf));
sprintf(buf, "i am client, this is %dth msg.\n", ++num);
int size = send(cfd, buf, strlen(buf), 0);
printf("client say: %s\n", buf);
// 接收数据
size = recv(cfd, buf, sizeof(buf), 0);
if (size == -1)
{
perror("recv");
exit(-1);
}
else if (size == 0)
{
printf("client disconnect...\n");
break;
}
else if (size > 0)
{
printf("server say : %s\n", buf);
}
sleep(1);
}
close(cfd);
return 0;
}
UDP通信流程如下:
UDP通信时使用到的API有:
#include
#include
ssize_t sendto(int sockfd,const void *buf,size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd,void *buf,size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen )
参数说明:
sockfd
:通信的socket fd
;buf
:要发送或接收的数据;len
:发送数据或接收数据的长度;flags
:一般设置为0;dest_addr
:通信的另外一端的地址消息;src_addr
:保存另外一端的地址信息;也可以指定为NULL,表示不需要addrlen
:dest_addr
或src_addr
地址的内存大小;返回值:
sento()
:成功返回发送的字节数,失败返回-1;
recvfrom()
:成功返回收到的字节数,失败返回-1;
UDP通信示例:
server.c
:
#include
#include
#include
#include
#include
int main()
{
// 创建socket
int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
if (sfd == -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;
int ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
// 通信
char buf[1024] = {0};
while (1)
{
memset(buf, 0, sizeof(buf));
// 接收消息
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
ssize_t size = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &len);
if (size == -1)
{
perror("recvfrom");
break;
}
// 输出客户端信息
char cip[16] = {0};
printf("client ip : %s, port : %d\n",
inet_ntop(AF_INET, &caddr.sin_addr.s_addr, cip, sizeof(cip)), ntohs(caddr.sin_port));
printf("recv msg : %s\n", buf);
// 发送数据
size = sendto(sfd, buf,strlen(buf)+1,0,(struct sockaddr*)&caddr,sizeof(caddr));
if(size == -1)
{
perror("sendto");
break;
}
}
close(sfd);
return 0;
}
client.c
:
#include
#include
#include
#include
#include
int main()
{
// 创建socket
int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
if (sfd == -1)
{
perror("socket");
exit(-1);
}
// 通信
// 接收消息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);
socklen_t len = sizeof(saddr);
char buf[1024] = {0};
int num = 0;
while (1)
{
memset(buf, 0, sizeof(buf));
sprintf(buf, "UDP : i am client, this is %dth msg.\n", ++num);
// 发送数据
ssize_t size = sendto(sfd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));
if (size == -1)
{
perror("sendto");
break;
}
size = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr *)&saddr, &len);
if (size == -1)
{
perror("recvfrom");
break;
}
printf("recv msg : %s\n", buf);
sleep(1);
}
close(sfd);
return 0;
}
广播:向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1;
广播只能在局域网中使用;
客户端需要绑定服务器广播使用的端口才可以接收到广播的消息;
使用setsockopt()
函数可以设置广播属性,
把该函数的参数level
设置为SOL_SOCKET
,
参数optname
设置为SO_BROADCAST
,
参数optval
设置为1表示允许发送广播,值为0表示不允许发送广播;
广播流程:
广播使用示例:
server.c
#include
#include
#include
#include
#include
int main()
{
// 创建socket
int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
if (sfd == -1)
{
perror("socket");
exit(-1);
}
// 设置广播属性
int optval = 1;
setsockopt(sfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval)); // 设置允许广播
// 创建一个广播的地址
struct sockaddr_in broadcast_addr;
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.57.255", &broadcast_addr.sin_addr.s_addr); // 192.168.57.255这个IP地址中的主机ID部分全部为1,即255
// 通信
char buf[1024] = {0};
int num = 0;
while (1)
{
memset(buf, 0, sizeof(buf));
sprintf(buf, "i am server, this is %dth msg.\n", ++num);
// 发送数据
ssize_t size = sendto(sfd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr));
if (size == -1)
{
perror("sendto");
break;
}
printf("广播的数据 : %s\n", buf);
sleep(1);
}
close(sfd);
return 0;
}
client.c
#include
#include
#include
#include
#include
int main()
{
// 创建socket
int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
if (sfd == -1)
{
perror("socket");
exit(-1);
}
// 客户端绑定本地的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(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
socklen_t len = sizeof(addr);
char buf[1024] = {0};
int num = 0;
while (1)
{
memset(buf, 0, sizeof(buf));
// 接收数据
ssize_t size = recvfrom(sfd, buf, sizeof(buf), 0, NULL, NULL);
if (size == -1)
{
perror("recvfrom");
break;
}
printf("recv msg : %s\n", buf);
}
close(sfd);
return 0;
}
单播地址标识单个IP端口,广播地址标识某个子网的所有IP接口,多播/组播标识一组IP接口。
单播和广播的寻址方案是两个极端,多播则在两者之间提供一种折中方案。
多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。
另外,广播一般局限于局域网内使用,多播既可以用于局域网,也可以跨广域网使用。
注意:客户端需要加入多播组,才能接收到多播的数据;
组播地址:IP多播通信必须依赖于IP多播地址,在 IPv4中范围从224.0.0.0
到239.255.255.255
,并被划分为局部连接多播地址、预留地址和管理权限多播地址三类。
IP地址 | 说明 |
---|---|
234.0.0.0 ~ 224.0.0.255 |
局部链接多播地址,是为路由协议和其它用途保留的地址,路由器并不转发属于此范围的IP包 |
224.0.1.0 ~ 224.0.1.255 |
预留多播地址,公用组播地址,可用于internet,使用前需要申请 |
224.0.2.0 ~ 238.255.255.255 |
预留多播地址,用户可用组播地址,临时组地址,全网范围内有效 |
239.0.0.0 ~ 239.255.255.255 |
本地管理组播地址,可供组织内部使用,类似于私有IP地址,不能用于internet,可限制多播范围 |
设置组播使用setsockopt
函数
服务器端设置多播信息时,函数参数设置:
level
设置为IPPROTO_IP
;optnam
设置为IP_MULTICAST_IF
;设置组播外出接口optval
是结构体struct in_addr
;客户端加入到多播组,函数参数设置:
level
设置为IPPROTO_IP
;optnam
设置为IP_ADD_MEMBERSHIP
,加入到多播组;optval
是结构体struct ip_mreqn
;struct ip_mreq
{
struct in_addr imr_multiaddr; //组播的IP地址
struct in_addr imr_interface; //加入的客服端主机IP地址,本地的IP地址
};
组播流程:
组播示例:
server.c
:
/**
* @file 1server_multi.c
* @author zoya ([email protected])
* @brief UDP通信组播-服务端
* @version 0.1
* @@date: 2022-10-18
*
* @copyright Copyright (c) 2022
*
*/
#include
#include
#include
#include
#include
int main()
{
// 创建socket
int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
if (sfd == -1)
{
perror("socket");
exit(-1);
}
// 设置多播属性,设置外出接口
struct in_addr optval;
// 初始化多播地址
inet_pton(AF_INET,"239.0.0.10",&optval.s_addr);
setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_IF, &optval, sizeof(optval)); // 设置组播外出接口
// 初始化客户端地址信息
struct sockaddr_in caddr;
caddr.sin_family = AF_INET;
caddr.sin_port = htons(9999);
inet_pton(AF_INET, "239.0.0.10", &caddr.sin_addr.s_addr);
// 通信
char buf[1024] = {0};
int num = 0;
while (1)
{
memset(buf, 0, sizeof(buf));
sprintf(buf, "i am server, this is %dth msg.\n", ++num);
// 发送数据
ssize_t size = sendto(sfd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&caddr, sizeof(caddr));
if (size == -1)
{
perror("sendto");
break;
}
printf("组播的数据 : %s\n", buf);
sleep(1);
}
close(sfd);
return 0;
}
client.c
:
/**
* @file 1client_multi.c
* @author zoya ([email protected])
* @brief UDP通信广播-客户端
* @version 0.1
* @@date: 2022-10-18
*
* @copyright Copyright (c) 2022
*
*/
#define _XOPEN_SOURCE 500
#include
#include
#include
#include
#include
struct ip_mreq
{
struct in_addr imr_multiaddr; //多播组的IP地址
struct in_addr imr_interface; //加入的客服端主机IP地址
};
int main()
{
// 创建socket
int sfd = socket(AF_INET, SOCK_DGRAM, 0); // 设置为UDP通信数据报
if (sfd == -1)
{
perror("socket");
exit(-1);
}
// 客户端绑定本地的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(sfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
// 加入到多播组
struct ip_mreq op;
inet_pton(AF_INET,"239.0.0.10",&op.imr_multiaddr.s_addr);
op.imr_interface.s_addr = INADDR_ANY;
setsockopt(sfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&op,sizeof(op));
socklen_t len = sizeof(addr);
char buf[1024] = {0};
int num = 0;
while (1)
{
memset(buf, 0, sizeof(buf));
// 接收数据
ssize_t size = recvfrom(sfd, buf, sizeof(buf), 0, NULL, NULL);
if (size == -1)
{
perror("recvfrom");
break;
}
printf("recv msg : %s\n", buf);
}
close(sfd);
return 0;
}