1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > 【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写

时间:2018-11-16 04:05:20

相关推荐

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写

参考连接:/study/live/504/2/16。

【Linux】网络编程一:网络结构模式、MAC/IP/端口、网络模型、协议及网络通信过程简单介绍

【Linux】网络编程二:socket简介、字节序、socket地址及地址转换API

【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写

文章目录

七, TCP通信7.1 TCP通信流程7.1.1 套接字相关函数7.1.2 TCP通信实现示例 7.2 TCP通信中的三次握手和四次挥手7.2.1 TCP三次握手7.2.2 滑动窗口7.2.3 四次挥手 7.3 实现并发服务器7.4 TCP状态转换7.5 端口复用 setsockopt7.6 I/O多路复用7.6.1 常见的I/O模型7.6.2 NIO中的多路复用 select/poll/epoll 7.7 本地套接字 八, UDP通信8.1 UDP通信流程及相关API介绍8.2 广播8.3 组播/多播

七, TCP通信

7.1 TCP通信流程

TCP和UDP都是传输层的协议,是传输层比较常用的协议。

TCP通信流程:

服务器端,被动接受连接 创建一个用于监听的套接字(就是一个文件描述符),监听客户端的连接;socket()将监听的文件描述符和本地IP、端口绑定(IP和端口即服务器的地址信息),客户端连接服务器时使用的就是这个IP和端口;bind()设置监听,此时监听的文件描述符fd开始工作;listen()阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字;accept()通信,接受数据read()/recv()、发送数据write()/send();通信结束,断开连接;close() 客户端,主动发送请求 创建用于通信的套接字;socket()主动连接服务器,需要指定连接的服务器的 IP 和 端口;connect()连接成功,客户端直接和服务器通信,发送数据write()/send()、接受数据read()/recv();通信结束,断开连接;close()

7.1.1 套接字相关函数

#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>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()函数介绍:

bind()函数介绍:

listen()函数介绍:

accept()函数介绍:

connect()函数介绍:

7.1.2 TCP通信实现示例

服务器端server.c

/*** @file server.c* @author zoya (2314902703@)* @brief 实现TCP服务器端的通信* @version 0.1* @@date: -10-09** @copyright Copyright (c) **/#include <arpa/inet.h>#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h>#include <string.h>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 1ret = inet_pton(AF_INET, "192.168.109.130", &addr.sin_addr.s_addr); // 字符串转换为整型IP地址if (ret != 1){perror("inet_pton");exit(-1);}#elseaddr.sin_addr.s_addr = INADDR_ANY; // 表示0.0.0.0,表示无线和网卡都绑定#endifaddr.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 (2314902703@)* @brief tcp通信 客户端* @version 0.1* @@date: -10-09** @copyright Copyright (c) **/#include <stdio.h>#include <arpa/inet.h>#include <unistd.h>#include <string.h>#include <stdlib.h>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;}

7.2 TCP通信中的三次握手和四次挥手

7.2.1 TCP三次握手

TCP是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的”连接“,其实是客户端和服务器的内存里保存的一份关于对方的信息,如IP地址、端口号等。

TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数,这些参数可以放在TCP头部。

TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接,采用四次挥手来关闭一个连接。

三次握手的目的是保证双方互相之间建立了连接。

三次握手发生在客户端请求连接时,调用connect()是,底层会进行三次握手。

三次握手保障了客户端和服务器互相了解自己及对方的收、发信息没有问题。

16位端口号-port number:告知主机报文段来自哪里(源端口号)以及给哪个上层协议或应用程序(目的端口)。进行TCP通信时,客户端通常使用系统自动选择的临时端口号。32位序号-sequence number:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系统初始化位某个随机值ISN(Initial Sequence Number,初始序号值)。那么在该传输方向上(从A到B),后续的TCP报文段中序号值将被系统设置成ISN加上 该报文段所携带数据的第一个字节在整个字节流中的偏移。 比如,某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是ISN+1025。另外一个传输方向(从B到A)的TCP报文段的序号值也具有相同的含义。 32位确认号-acknowledgement number:用作对另一方发送来的TCP报文段的响应,其值是收到的TCP报文段的序号值 + 标志位长度(SYN/FIN) + 数据长度。 假设主机A和主机B之间进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,而且包含对B发送来的TCP报文段的确认号。反之,B发送出的TCP报文段也同样携带自己的序号和对A发送来的报文段的确认序号。 4位头部长度-head length:标识TCP头部有多少个32bit(4字节)。6位标志位包含如下:URG:标识紧急指针(urgent pointer)是否有效;ACK:标识确认好是否生效,称携带ACK标志的TCP报文段为确认报文段。PSH:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区)。RST:表示要求对方重新建立连接,称携带RST标志的TCP报文段为复位报文段。SYN:表示请求建立一个连接,称携带SYN标志的TCP报文段为同步报文段。FIN:表示通知对方本端要关闭连接了,称携带FIN标志的TCP报文段为结束报文段。 16位窗口大小-window size:是TCP流量控制的一个手段,这里说的窗口,指的是接收通告窗口(Receiver Window,RWND)。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。16位校验和-TCP checksum:由发送端填充,接收端对TCP报文段执行CRC算法以校验TCP报文段在传输过程中是否损坏。需要注意,这个校验不仅包括TCP头部,也包括数据部分,这也是TCP可靠传输的一个重要保障。16位紧急指针-urgent pointer:是一个整型的偏移量,和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。确切的说,这个字段是紧急指针相对当前序号的偏移,可以称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

第一次握手

客户端发送请求连接,SYN=1;生成一个随机的32位的序号,seq=J;

第二次握手

服务器端接收客户端的连接;ACK=1;服务器回发一个确认序号,ack=客户端序号 + 数据长度 + SYN/FIN(按一个字节);服务器端向客户端发起连接请求,SYN=1;服务器生成一个随机序号,seq=K;

第三次握手

客户端应答服务器的连接请求;ACK=1客户端回复收到,ack = 服务器端的序号 + 数据长度 + SYN/FIN(按一个字节)

问题1:如何确定发送的数据是完整的?

问题2:如何确定接收数据的顺序和发送数据的顺序是一直的?

通过序号和确认序号,可以确定发送的数据是完整的,也可以确定接收数据的顺序和发送数据的顺序是一致的。

7.2.2 滑动窗口

滑动窗口,Sliding window,是一种流量控制技术,早期的网络通信中,通信双方不会考虑网络的拥挤情况,直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发送不了数据,所以有了滑动窗口机制来解决此问题。

滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包,称为窗口尺寸。

TCP中采用使用滑动窗口进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方一般不能再发送数据报。

滑动窗口是TCP中实现诸如ACK确认、流量控制、拥塞控制的承载结构。

窗口可以简单理解为缓冲区的大小。滑动窗口的大小是随着发送数据和接收数据变化的,每一端通信的双方都有发送缓冲区和接收数据的缓冲区。对服务器来说,有发送缓冲区和接收缓冲区;对客户端来说也有发送缓冲区和接收缓冲区;那么对应的,服务器端和客户端都有发送缓冲区的窗口和接收缓冲区的窗口。

7.2.3 四次挥手

四次挥手发生在断开连接时,程序中调用close()会使用TCP协议进行四次挥手。

客户端和服务端都可以主动发起断开连接,谁先调用close(),谁就是发起。

在TCP连接时,采用三次握手建立的连接是双向的,在断开的时候也需要双向断开连接。

如下:

客户端向服务器发送断开连接,并发送数据;FIN=1,seq=M;服务器收到客户端的断开连接要求,向客户端发送确认;ACK=1,ack=M+1;该操作后客户端不能再向服务器端发送数据,可以接收数据,但是可以发送报文头以回复服务器端的断开连接要求;服务器端向客户端发送断开连接,并发送数据;FIN=1,seq=N;客户端收到服务器端的断开连接要求,向服务器端发送确认;ACK=1,ack=N+1;该操作后双方断开连接,不能发送和接收数据;

7.3 实现并发服务器

要实现TCP通信服务器处理并发任务,可以使用多线程或者多进程解决。

解决思路1:多进程解决

一父进程;多个子进程;父进程等待并接受客户端的连接;多个子进程完成通信,接受客户端的连接;

示例代码:

client.c

/*** @file 1client.c* @author zoya(2314902703@)* @brief TCP通信客户端,循环向服务器发送消息,并接收服务器返回的消息* @version 0.1* @date -10-10** @copyright Copyright (c) **/#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <arpa/inet.h>int main(){int ret = -1;// 创建socketint 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转换为网络字节序portret = inet_pton(AF_INET, g_ip, &addr.sin_addr.s_addr); // 格式转换,主机字节序ip转换为网络字节序ipif (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(2314902703@)* @brief 服务器端实现并发处理,进程实现* @version 0.1* @date -10-13** @copyright Copyright (c) **/#define _XOPEN_SOURCE 500#include <stdio.h>#include <arpa/inet.h>#include <string.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/wait.h>#include <sys/stat.h>#include <errno.h>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(){// 捕捉信号 SIGCHLDstruct sigaction act;act.sa_flags = 0;sigemptyset(&act.sa_mask);act.sa_handler = recyChild;sigaction(SIGCHLD, &act, NULL);// 创建SOCKETint 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(2314902703@)* @brief 多线程实现并发服务器* @version 0.1* @date -10-13** @copyright Copyright (c) ** 没有一个连接就创建一个线程,在线程中接受或发送数据* 主线程连接通信**/#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <pthread.h>#include <errno.h>#include <arpa/inet.h>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(){// 创建SOCKETint 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;}

7.4 TCP状态转换

TCP状态转换发生在三次握手和四次挥手过程中。

三次握手

客户端发送连接请求,客户端处于SYN_SENT状态;服务器开始处于监听LISTEN状态,收到客户端的连接请求,变为SYN_RCVD状态;服务器向客户端发送确认和连接请求,客户端变为ESTABLISHED状态;客户端向服务器发送确认,服务器变为ESTABLISHED状态;

四次挥手

客户端发送断开连接请求(FIN=1),状态变为FIN_WAIT_1;服务端接收到FIN请求后,服务端变为CLOSE_WAIT(等待关闭),服务端回复客户端ACK相应;客户端收到服务端的响应,状态变为FIN_WAIT_2;服务端发送断开连接请求(FIN=1),服务端状态变为LAST_ACK;客户端收到服务端的请求后专改变为TIME_WAIT,并向客户端发送ACK; TIME_WAIT:定时经过2倍报文段时间,2MSL。

主动断开连接的一方,最后进入一个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 <aya/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不考虑文件描述符的引用计数,直接关闭文件描述符。也可以选择终止一个方向的连接,只终止读或只终止写。

如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,所有进程都调用close,套接字被释放。

在多进程中如果一个进程调用了shurdown(sfd,SHUT_RDWR),其它进程将无法进行通信,但如果一个进程close(sfd)将不会影响其它进程。

7.5 端口复用 setsockopt

端口复用最常用的用途:

防止服务器重启时之前绑定的端口还未释放;程序突然退出而系统没有释放端口;

#include <sys/types.h>#include <sys/sockt.h>// 设置端口复用,也可以设置端口状态int setsockopt(int sockfd, int level, int optname, consr void *optval, socklen_t *optlen); // 该函数仅用于套接字

sockfd:指向一个打开的套接字描述符;level:级别,使用SOL_SOCKET(端口复用的级别);optname:选项名称,端口复用使用以下:SO_RUSEADDRSO_RUSEPORToptval:端口复用的值,整型; 1 表示可以复用;0 表示不可复用;optlen:optval参数的大小;

端口复用设置的时机在服务器绑定端口之前。

socket() // 创建socketsetsockopt() // 设置端口复用bind() // 绑定

端口复用示例:

server.c中设置端口复用:

/*** @file 2server.c* @author zoya(2314902703@)* @brief 接收客户端的消息,并转换消息* @version 0.1* @date -10-14** @copyright Copyright (c) **/#define _XOPEN_SOURCE 500#include <stdio.h>#include <stdlib.h>#include <string.h>#include <arpa/inet.h>#include <sys/types.h>#include <unistd.h>#include <ctype.h>#include <signal.h>#include <sys/wait.h>#include <errno.h>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);// 创建socketint 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;}

7.6 I/O多路复用

I/O多路复用有时也称为I/O多路转接。

I/O多路复用使 程序能够同时监听多个文件描述符,能够提高程序的性能。Linux实现I/O多路复用的系统调用主要有selectpollepoll

7.6.1 常见的I/O模型

阻塞等待 BIO-Blocking I/O

不占用CPU宝贵的时间片;但是同一时刻只能处理一个操作,效率低;

解决方案:可以使用多线程或者多进程方式解决; 线程或进程会消耗一定的系统资源;线程或进程调度会消耗CPU资源;

非阻塞,忙轮询 NIO-Non-Blocking I/O

提高了程序的执行效率;但是需要占用更多的CPU和系统资源,每循环内有O(n)的系统调用;;

解决方案:使用IP多路转接技术,select/poll/epoll select/poll:仅通知有几个数据到了,需要自己遍历是在哪些读缓冲区中;epoll:通知哪些读缓冲区有数据;

IO复用

信号驱动

异步

7.6.2 NIO中的多路复用 select/poll/epoll

select

构造一个文件描述符列表,将要监听的文件描述符添加到该列表中。调用系统函数select()监听该列表中的文件描述符,直到这些文件描述符中的一个或多个进行I/O操作时,该函数才返回。select()是阻塞的,且对文件描述符的检测的操作是由内核完成的。在返回时,该函数会告诉进程有多少文件描述符要进行I/O操作。

#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);

select函数参数介绍:

nfd:委托内核检测的最大文件描述符的值 +1.

readfds:要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读属性;

检测读数据;对应的是对方发送的数据,检测读缓冲区。

wrfdsite:要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写属性;

委托内核检测写缓冲区是不是还可以写数据(即检测写缓冲区是否满了);一般不检测写缓冲区,设置为NULL;

exceptfds:检测发生异常的文件描述符的集合,一般不使用设置为NULL。

timeout:设置的超时时间;

如果为NULL表示永久阻塞,直到检测到文件描述符有变化;

如果tv_sectv_usec都为0表示不阻塞;

如果tvsec>0tvusec>0表示阻塞对应的时间;

struct timeval{long tv_sec;long tv_usec;}

select返回值:

-1:表示失败;>0:表示检测的集合中有n个文件描述符发生了变化

如下函数是对二进制位的一些操作:

void FD_CLR(int fd,fd_set *set); // 对fd对应的标志位置为0int 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(2314902703@)* @brief TCP通信服务端:select实现* @version 0.1* @date -10-14** @copyright Copyright (c) **/#include <stdio.h>#include <stdlib.h>#include <string.h>#include <arpa/inet.h>#include <unistd.h>#include <sys/time.h>#include <sys/types.h>#include <sys/select.h>int main(){// 创建socketint 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); // 标志位全部置为0FD_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的缺点:

每次调用,需要把fd集合从用户态拷贝到内核态,如果fd很多,开销很大;每次调用,都需要在内核遍历传递进来的fd集合,开销在fd很多时也很大;select支持的文件描述符数量太小,默认是1024;fds集合不能重用,每次都需要重置;

poll

poll是对select的改进。

#include <poll.h>struct pollfd{int fd; // 委托内核检测的文件描述符short events; // 委托内核检测文件描述符的什么事件short revents; // 文件描述符实际发生的事情}int poll(struct pollfd *fds, nfds_t nfds,int timeout);

参数说明:

fdsstruct pollfd结构体数组,是一个需要检测的文件描述符的集合;没有个数1024的限制;nfds:第一个参数中最后一个有效元素的下标 + 1timeout:阻塞时长,0表示不阻塞;-1表示阻塞,当检测到需要检测的文件描述符有变化,解除阻塞;>0的值表示阻塞的时长,单位:毫秒;

返回值:

-1表示失败;>0表示检测到集合中有文件描述符发生变化

poll的缺点:

每次需要把文件描述符数组从用户态拷贝到内核态,开销比较大。主动遍历,每次在内核中都会主动遍历哪些文件描述符发生改变

poll使用示例:

// server.c/*** @file 1poll_server.c* @author zoya (2314902703@)* @brief TCP通信服务端:poll实现IO多路复用* @version 0.1* @date -10-17** @copyright Copyright (c) **/#include <stdio.h>#include <stdlib.h>#include <string.h>#include <poll.h>#include <unistd.h>#include <arpa/inet.h>int main(){// 创建socketint 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 <sys/epoll.h>struct eventpoll{...struct rb_root rbr; // 采用红黑树的数据结构,查找效率比较高struct list_head rdlist; // 记录需要检测的文件描述符,双链接的形式...};struct union epoll_data{void *ptr;int fd; // 常用的是fduint32_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,水平触发,是缺省的工作方式,同时支持blockno-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不通知

7.7 本地套接字

本地套接字用来实现本地进程间通信(有关系和没有关系的进程间通信)。本地套接字和网络套接字类似,一般采用TCP通信流程。

本地套接字通信流程:

服务端 创建监听套接字,int lfd = socket(AF_UNIX,SOCK_STREAM,0);监听的套接字绑定本地的套接字文件,本地地址struct sockaddr_un addrbind(lfd,addr,len);绑定成功后,指定的sun_path中的套接字文件会自动生成 监听是否有客户端连接,listen(lfd,128);等待并接受客户端连接请求,使用本地地址,int cfd = accept(lfd,caddr,len);通信,接收(read/recv)或发送(write/send)数据;关闭连接 客户端 创建通信的套接字,int fd = socket(AF_UNIX,SOCK_STREAM,0);监听的套接字绑定本地的IP端口本地地址struct sockaddr_un addrbind(lfd,addr,len);绑定成功后,指定的sun_path中的套接字文件会自动生成 请求连接服务器,connet(fd,saddr,len);通信,发送(write/send)或者接收(read/recv)数据;关闭连接

本地套接字通信示例:

server.c

/*** @file 2server_ipc.c* @author zoya (2314902703@)* @brief 本地套接字服务端* @version 0.1* @@date: -10-18** @copyright Copyright (c) **/#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/un.h>#define PATH_UNIX 100int main(){// 删除服务端sockunlink("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 (2314902703@)* @brief 本地套接字通信客户端* @version 0.1* @@date: -10-18** @copyright Copyright (c) **/#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/un.h>int main(){// 删除客户端sockunlink("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通信

8.1 UDP通信流程及相关API介绍

UDP通信流程如下:

UDP通信时使用到的API有:

#include <sys/types.h>#include <sys/socket.g>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 fdbuf:要发送或接收的数据;len:发送数据或接收数据的长度;flags:一般设置为0;dest_addr:通信的另外一端的地址消息;src_addr:保存另外一端的地址信息;也可以指定为NULL,表示不需要addrlendest_addrsrc_addr地址的内存大小;

返回值:

sento():成功返回发送的字节数,失败返回-1;

recvfrom():成功返回收到的字节数,失败返回-1;

UDP通信示例:

server.c

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>int main(){// 创建socketint 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 <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>int main(){// 创建socketint 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;}

8.2 广播

广播:向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1;

广播只能在局域网中使用;

客户端需要绑定服务器广播使用的端口才可以接收到广播的消息;

使用setsockopt()函数可以设置广播属性,

把该函数的参数level设置为SOL_SOCKET

参数optname设置为SO_BROADCAST

参数optval设置为1表示允许发送广播,值为0表示不允许发送广播;

广播流程:

广播使用示例:

server.c

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>int main(){// 创建socketint 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 <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>int main(){// 创建socketint 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;}

8.3 组播/多播

单播地址标识单个IP端口,广播地址标识某个子网的所有IP接口,多播/组播标识一组IP接口。

单播和广播的寻址方案是两个极端,多播则在两者之间提供一种折中方案。

多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。

另外,广播一般局限于局域网内使用,多播既可以用于局域网,也可以跨广域网使用。

注意:客户端需要加入多播组,才能接收到多播的数据;

组播地址:IP多播通信必须依赖于IP多播地址,在 IPv4中范围从224.0.0.0239.255.255.255,并被划分为局部连接多播地址、预留地址和管理权限多播地址三类。

设置组播使用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 (2314902703@)* @brief UDP通信组播-服务端* @version 0.1* @@date: -10-18* * @copyright Copyright (c) * */#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>int main(){// 创建socketint 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 (2314902703@)* @brief UDP通信广播-客户端* @version 0.1* @@date: -10-18* * @copyright Copyright (c) * */#define _XOPEN_SOURCE 500#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>struct ip_mreq{struct in_addr imr_multiaddr; //多播组的IP地址struct in_addr imr_interface; //加入的客服端主机IP地址};int main(){// 创建socketint 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;}

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。