[Linux网络编程]TCP/UDP、I/O复用、本地套接字

Socket

套接字

套接字文件描述符sockfd:指向内存中的一块缓冲区,主机间的通信就是在往这块缓冲区中读写数据

目的地址sockaddr_in :就是服务端的地址,是一个结构体,其中定义了主机地址,端口,等数据

服务端需要将地址绑定到套接字的文件描述符上(bind),后续的传输就会在这个文件描述符上接收(listen,accept,recv,send)
客户端需要将文件描述符和给定的服务端地址(connect),然后读写文件描述符(recv,send)

struct sockaddr_in {
short int sin_family; // 地址族
unsigned short int sin_port; // 端口号
struct in_addr sin_addr; // IP 地址
unsigned char sin_zero[8]; // 保留字节
};

sockaddr_in 结构体包含以下几个重要的成员:
sin_family:指定地址族,通常设置为 AF_INET。
sin_port:存储端口号,必须是网络字节序(Network Byte Order)。
sin_addr:存储IP地址,也是网络字节序。
sin_zero:8字节的填充字段,保留未使用,通常设置为0。

sin_addr 是一个 in_addr 结构体,用于存储 IP 地址。in_addr 结构体定义如下:
这个里面的东西不需要手工记,直接用函数转换

struct in_addr {
	union {
		struct {
			unsigned char s_b1, s_b2, s_b3, s_b4;
		} S_un_b;
		struct {
			unsigned short s_w1, s_w2;
		} S_un_w;
		unsigned long S_addr;
	} S_un;
};

对于网络编程函数中的 const struct sockaddr *dest_addr,这是一个通用结构体
在实际编程中,为了方便操作特定协议的地址,通常会使用特定的地址结构体。
例如,对于 IPv4 地址,会使用 struct sockaddr_in;对于 IPv6 地址,会使用 struct sockaddr_in6。
这类地址在传入struct sockaddr 时需要强制类型转换(struct sockaddr *)&cliaddr,变成通用结构体

// 服务器的地址信息
    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);
 
    // 发送数据
    char sendBuf[128];
    sprintf(sendBuf, "hello , i am client %d \n", num++);
    sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr)); 

使用示例:

#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

int main() {
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr)); // 初始化结构体
	addr.sin_family = AF_INET; // 设置地址族为 IPv4
	addr.sin_port = htons(8080); // 设置端口号
	addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // 设置 IP 地址
	
	printf("IP Address: %s\n", inet_ntoa(addr.sin_addr));
	return 0;
}

ip地址的相互转换

网络编程中经常涉及到网络字节序(大端序)和主机字节序(小端序或大端序)的转换。sockaddr_in 结构体中的 sin_port 和 sin_addr 成员都需要使用网络字节序。为了进行转换,通常会使用以下几个函数:

htons():将端口号从主机字节序转换为网络字节序。

htonl():将长整型数据从主机字节序转换为网络字节序。

inet_addr():将点分十进制的IP地址字符串转换为网络字节序的整数值。

inet_ntoa():将网络字节序的整数值转换为点分十进制的IP地址字符串。

#include <arpa/inet.h>

// p:点分十进制的IP字符串,n:表示network,网络字节序的整数

int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面

// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

大端小端转换

大端:高位字节存储在低位地址上
小端:低位字节存储在低位地址上

网络上面传输的数据默认为大端
到主机后通过调用函数根据系统情况自动转换

#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t host); // 主机host字节序 - 网络net字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 - 网络字节序

// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 - 网络字节序

TCP分解

在这里插入图片描述

  • 16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或
    应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号。
  • 32 位序号(sequence number):本报文段第一个字节的编号
    • 例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。
  • 32 位确认号(acknowledgement number):下次希望接收到的报文段的第一个字节的序号。其值是收到的 TCP 报文段的序号值+ 数据长度 +1。
  • 4 位头部长度(head length):由于TCP头部长度不固定,因此需要标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示15,所以 TCP 头部最长是60 字节。
  • 六位保留,目前置0
  • 6 位标志位包含如下几项:
    • URG 标志,表示紧急指针(urgent pointer)是否有效。
    • ACK 标志,表示确认号是否有效。连接后所有传送的报文段都必须把ACK置1
    • PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,而不是缓存满了再交付。
    • RST 标志,表示当前发生严重错误,要求对方释放连接后再重新建立连接。复位报文段。
    • SYN 标志,表明一个连接请求(ACK=0,SYN=1)或连接接受(ACK=1, SYN=1)报文端。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
    • FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文
      段。
  • 16 位窗口大小(window size):接收通告窗口(Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
  • 16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验
    TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。
    这也是 TCP 可靠传输的一个重要保障。
  • 16 位紧急指针(urgent pointer):从第一个字节到紧急指针所指字节就是紧急数据
  • 选项与填充:选项长度可变,TCP只规定一种选项,最大报文段长度MSS,是报文段中数据字段的最大长度。填充让长度为4B的整数倍

三次握手

第一次:客户端发送请求(SYN_SENT, connect())
服务端:可以确定自己收信正常(可以收到消息)(LISTEN,listen()------>SYN_RCVD)
SYN=1, seq=x

第二次:服务端发同步信息
客户端:可以确定自己的发信正常,收信正常(发出的请求有回应,且自己能收到回复的消息)(ESTABLISHED)
SYN=1, ACK=1, seq=y, ack=x+1

第三次:客户端发同步信息
服务端:可确定自己发信正常(发出的请求有回应) (ESTABLISHED)
ACK=1, seq=x+1, ack=y+1

简洁版:第一次握手,客户端发送SYN包到服务器;第二次握手,服务器收到SYN包,回复一个SYN+ACK包;第三次握手,客户端收到服务器的SYN+ACK包后,回复一个ACK包,完成三次握手。

在这里插入图片描述
在这里插入图片描述

四次挥手

第一次:客户端提出关闭请求,发出释放报文段,FIN=1,消耗一个序号
此时客户端无法发送数据,但是可以接收数据(FIN-wait1, close())
FIN=1, seq=u

第二次:服务器确认收到了客户端的请求,返回一个确认报文
此时客户端无法发送数据,但是可以接收数据(FIN-wait2)
服务端进度关闭等待状态(CLOSE-WAIT),服务器此时可继续发送文件数据
ACK=1, seq=v, ack=u+1

第三次:服务器发起关闭请求,FIN=1,不消耗序号
服务器发送数据后seq=w,服务器进入最终确认状态(LAST-ACK, close()),无法发送文件数据,可以接收数据
客户端接收到服务端的结束请求(FIN-wait2),可以发送确认数据
FIN = 1, ACK=1, seq =w, ack=u+1

第四次:客户端发送一个结束确认,确认收到了服务端的结束请求,关闭连接
客户端彻底关闭发送与接受,进入TIME-WAIT, 等待两个最长报文寿命2MSL后关闭
服务端仍可以接受数据,,等待接受收到关闭确认后直接关闭
ACK=1, seq=u+1, ack=w+1

简洁版:第一次挥手,客户端发送FIN包到服务器;第二次挥手,服务器收到FIN包,回复一个ACK包;第三次挥手,服务器发送FIN包到客户端;第四次挥手,客户端收到FIN包,回复一个ACK包,完成四次挥手。

TIME_WAIT存在的意义: 首先是保证可以收到FIN报文和发出ACK报文,其次防止如果该端口再次创建新连接,结果旧的报文刚到,这样会造成混乱。
在这里插入图片描述

TCP 通信的流程

服务器端 (被动接受连接的角色)

  1. 创建一个用于监听的套接字
    • 监听:监听有客户端的连接
    • 套接字:这个套接字其实就是一个文件描述符
  2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
    • 客户端连接服务器的时候使用的就是这个IP和端口
  3. 设置监听,监听的fd开始工作
  4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
  5. 通信
    • 接收数据
    • 发送数据
  6. 通信结束,断开连接

客户端(主动创建链接)

  1. 创建一个用于通信的套接字(fd)
  2. 连接服务器,需要指定连接的服务器的 IP 和 端口
  3. 连接成功了,客户端可以直接和服务器通信
    • 接收数据
    • 发送数据
  4. 通信结束,断开连接

常用函数

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);//需要使用close对文件描述符fd进行关闭  
	- 功能:创建一个套接字
	- 参数:
	- domain: 协议族
	AF_INET : ipv4
	AF_INET6 : ipv6
	AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
	- type: 通信过程中使用的协议类型
	SOCK_STREAM : 流式协议
	SOCK_DGRAM : 报式协议
	- protocol : 具体的一个协议。一般写0
	- SOCK_STREAM : 流式协议默认使用 TCP
	- SOCK_DGRAM : 报式协议默认使用 UDP
	- 返回值:
	- 成功:返回文件描述符,操作的就是内核缓冲区。
	- 失败:-1

int bind(int sockfd, const  struct sockaddr *addr, socklen_t addrlen); // socket命- 功能:绑定,将fd 和本地的IP + 端口进行绑定
	- 参数:
	- sockfd : 通过socket函数得到的文件描述符
	- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
	- addrlen : 第二个参数结构体占的内存大小

int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
	- 功能:服务端监听这个socket上的连接
	- 参数:
	- sockfd : 通过socket()函数得到的文件描述符
	- backlog : 未连接的和已经连接的和的最大值 
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
	- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
	- 参数:
	- sockfd : 用于监听的文件描述符
	- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
	- addrlen : 指定第二个参数的对应的内存大小
	- 返回值:
	- 成功 :用于通信的文件描述符
	- -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	- 功能: 客户端函数,客户连接到服务器需用这个 
	- 参数:
	- sockfd : 用于通信的文件描述符
	- addr : 客户端要连接的服务器的地址信息
	- addrlen : 第二个参数的内存大小
	- 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据

ssize_t read(int fd, void *buf, size_t count); // 读数据

recv()\ send() 更安全,更常用

在这里插入图片描述

状态转换一览

在这里插入图片描述

半连接状态:

当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2
状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发
送的数据,但是 A 已经不能再向 B 发送数据

#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(sfd) 将不会影响到其它进程。

端口复用

意义:让客户端可以从同一个端口和不同主机进行链接,通过端口复用可让多个进程绑定同一个端口。
可以将一个端口绑定到多个ip上
可以将同一个ip与其绑定的端口绑定到多个套接字上

#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *val, socklen_t
optlen);
参数:
- sockfd : 要操作的文件描述符
- level : 选项应用的协议 - SOL_SOCKET (套接字层次) - IPPROTO_TCP(TCP选项) - IPPROTO_IP(ip选项)
- optname : 选项的名称
- SO_REUSEADDR
- SO_REUSEPORT 
- val : opt的属性(整形)
- 1 : 可以复用
- 0 : 不可以复用
- optlen : optval参数的大小
端口复用,设置的时机是在服务器绑定端口之前。
setsockopt();
bind();

I/O多路复用

BIO

BIO (Block IO):同步阻塞IO。一般我们传统的JDK内置的Socket编程就是阻塞IO。其底层流程是:①创建socket接口,号为x,通过bind函数将接口号与端口号进行绑定,然后进行listn监听事件或者是read读事件,且会一直阻塞在该命令,直到有客户端连接或者发送数据。

缺点:如果是在单线程环境下,由于是阻塞地获取结果,只能有一个客户端连接。而如果是在多线程环境下,需要不断地新建线程来接收客户端,这样会浪费大量的空间。
.

NIO

NIO(NONBLOCK IO):同步非阻塞IO。非阻塞意味着程序无需等到结果,持续进行。其底层原理是:①同样与BIO相同创建Socket接口,号为x,绑定接口号与端口号,然后进行listen监听事件或者是读数据事件。②通过configureBlock函数传入参数false,底层命令为 fcntl(socket号,nonblock)将socket号标记为非阻塞。③循环执行。假如有客户端进行连接,则返回一个新的socket号,将新的socket号加入一个list中,然后遍历list中的元素查看有无发生read事件;如果没有客户端进行连接,则返回-1,代表没有客户端连接,再不断地循环。

缺点:需要遍历list中的每个集合查看有无监听的事件发生,时间复杂度为O(n),浪费CPU资源。

select/poll模式

进程通过告诉多路复用器(相当于用内核代理)(也就是select函数和poll函数)所有的socket号,多路复用器再去获取每一个socket的状态,当程序获取到某个socket号有事件发生了,则去该socket号上进行处理对应的事件,read事件或者是recived事件。
select函数与poll函数的区别是,前者底层是数组,所以有最大连接数的限制,后者是链表,无最大连接数的限制

缺点:①同样与NIO相同,需要遍历所有socket,O(N)复杂度。
②重复传递数据。
因为内核是无状态的,每次都要根据进程不断重复从用户态向内核态传递所有的socket号去遍历每一个socket,获取它们的状态。浪费资源与效率,可以使用一个记事本记录每个socket的监听事件。

select维护三个集合:读、写、异常集合
函数初始化后,内核拷贝所有数据,内核对事件进行轮询,获取到事件后将数据再拷贝回文件中(两次拷贝)
返回值是一个数值,告诉有几个文件已就绪,但是具体是哪个需要通过遍历判断

 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;         /* seconds */
                    long    tv_usec;        /* microseconds */
                };
                - NULL : 永久阻塞,直到检测到了文件描述符有变化
                - tv_sec = 0 tv_usec = 0, 不阻塞
                - tv_sec > 0 tv_usec > 0, 阻塞对应的时间
                    
       - 返回值 : 
            - -1 : 失败
            - >0(n) : 检测的集合中有n个文件描述符发生了变化

poll与select同理,两次拷贝

#include <poll.h>
 struct pollfd {
 int   fd;         
short events;     
short revents;    
};
 /* 委托内核检测的文件描述符 */
 /* 委托内核检测文件描述符的什么事件 */
 /* 文件描述符实际发生的事件 */
 struct pollfd myfd;
 myfd.fd = 5;
 myfd.events = POLLIN | POLLOUT;
 
 int poll(struct pollfd *fds, nfds_t nfds, int timeout);    
  • 参数:
  • fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
  • nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1- timeout : 阻塞时长
    0 : 不阻塞-1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
    >0 : 阻塞的时长- 返回值:-1 : 失败
    >0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化

epoll

(仅能用于linux平台)
数据读写发生在内核中或内存中的共享区域shm中,无需反复拷贝数据
无需遍历才能获取当前就绪的文件,而是直接获取到文件描述符
无最大文件描述符限制

三大函数:epoll_create()、epoll_ctl()、epoll_wait()

create:传入一个大于零的任意值,返回一个文件描述符

crl:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    - 参数:
       - epfd : epoll实例对应的文件描述符
       - op:
           EPOLL_CTL_ADD:  添加想对fd检测的事件
           EPOLL_CTL_MOD:  修改fd对应检测的事件
           EPOLL_CTL_DEL:  删除fd,此时事件可以置NULL
       - fd : 要检测的文件描述符
       - event : 检测文件描述符什么事

event结构体

 struct epoll_events {
   	uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
 };
常见的Epoll检测事件:
    - EPOLLIN:检测fd的读缓冲区是否有数据
    - EPOLLOUT:文件描述符的写缓冲区是否可写
    - EPOLLER:错误的发生
    - EPOLLET:边缘触发模式

注意data是一个union联合体,和发生的事件绑定在一起,可以是ctl中传入的那个fd

typedef union epoll_data {
	void   *ptr;
	int     fd;// 
	uint32_t    u32; 一般使用这个,
	uint64_t     u64;
} epoll_data_t;

wait:如果红黑树上有就绪状态的文件描述符,就返回描述符数组,如果没有就绪的文件,那么会一直阻塞

// 检测函数                
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int 
timeout);
  • 参数:
    - epfd : epoll实例对应的文件描述符
    - events : 传出参数,保存了发送了变化的文件描述符的信息
    - maxevents : 第二个参数结构体数组的大小
    - timeout : 阻塞时间
    - 0 : 不阻塞
    - -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
    - > 0 : 阻塞的时长(毫秒)

  • 返回值:
    - 成功,返回发送变化的文件描述符的个数 > 0
    - 失败 -1

几种fd:lfd:监听端口的socket
cfd:负责和客户端通信的socket
epfd:epoll模型创建的红黑树的fd

水平触发LT

如果不处理当前事件,那么将一直提示用户处理当前就绪进程

4.1 水平模式
水平模式可以简称为LT模式,LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。
在这种做法中,内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行IO操作了。如果我们不作任何操作,内核还是会继续通知使用者。

水平模式的特点:

读事件:
如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait()解除阻塞
当读事件被触发,epoll_wait()解除阻塞,之后就可以接收数据了
如果接收数据的buf很小,不能全部将缓冲区数据读出,那么读事件会继续被触发,直到数据被全部读出,如果接收数据的内存相对较大,读数据的效率也会相对较高(减少了读数据的次数)
因为读数据是被动的,必须要通过读事件才能知道有数据到达了,因此对于读事件的检测是必须的
写事件:
如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait()解除阻塞
写事件的触发发生在写数据之前而不是之后,被写入到写缓冲区中的数据是由内核自动发送出去的
如果写缓冲区没有被写满,写事件会一直被触发
因为写数据是主动的,并且写缓冲区一般情况下都是可写的(缓冲区不满),因此对于写事件的检测不是必须的

边缘触发ET

只提醒一次当前进程

4.2 边沿模式
边沿模式可以简称为ET模式,ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)。
如果我们对这个文件描述符做IO操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。

边沿模式的特点:

读事件:
如果有新数据进入到读缓冲区,读事件被触发,epoll_wait()解除阻塞
读事件被触发,可以通过调用read()/recv()函数将缓冲区数据读出
如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次
如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次
写事件:
当写缓冲区状态可写,写事件只会触发一次
如果写缓冲区被检测到可写,写事件被触发,epoll_wait()解除阻塞
写事件被触发,就可以通过调用write()/send()函数,将数据写入到写缓冲区中
写缓冲区从不满到被写满,期间写事件只会被触发一次
写缓冲区从满到不满,状态变为可写,写事件只会被触发一次
综上所述:epoll的边沿模式下 epoll_wait()检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。

作者: 苏丙榅
链接: https://subingwen.cn/linux/epoll/#4-2-%E8%BE%B9%E6%B2%BF%E6%A8%A1%E5%BC%8F
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

UDP

在网络编程里,sendsendtorecvrecvfrom是用于数据收发的关键函数。下面针对不同的编程语言和网络编程模型,详细阐述它们的用法。
UDP接收不需要提供来源的地址

1. C语言中这些函数在套接字编程里的用法

send函数

send 函数主要用于面向连接的套接字(如TCP),它会把数据发送到已经建立连接的套接字。

#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd:已连接的套接字描述符。
  • buf:要发送的数据缓冲区。
  • len:要发送的数据长度。
  • flags:可选标志,通常设为0。

sendto函数

sendto 函数适用于无连接的套接字(如UDP),可以指定目标地址。

#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd:套接字描述符。
  • buf:要发送的数据缓冲区。
  • len:要发送的数据长度。
  • flags:可选标志,通常设为0。
  • dest_addr:目标地址。
  • addrlen:目标地址的长度。

recv函数

recv 函数用于从已连接的套接字接收数据。

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:已连接的套接字描述符。
  • buf:接收数据的缓冲区。
  • len:缓冲区的长度。
  • flags:可选标志,通常设为0。

recvfrom函数

recvfrom 函数用于从无连接的套接字接收数据,同时可以获取发送方的地址。

#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd:套接字描述符。
  • buf:接收数据的缓冲区。
  • len:缓冲区的长度。
  • flags:可选标志,通常设为0。
  • src_addr:发送方的地址。
  • addrlen:发送方地址的长度。

总结

  • sendrecv 适用于面向连接的套接字(TCP),在使用前需要先建立连接。
  • sendtorecvfrom 适用于无连接的套接字(UDP),无需提前建立连接,每次发送和接收都要指定或获取对方地址。

UDP服务端设置

需要绑定一个输出的端口


    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    // 2.绑定
    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
        char recvbuf[128];
        char ipbuf[16];

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);

        // 接收数据
        int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);

        printf("client IP : %s, Port : %d\n", 
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
            ntohs(cliaddr.sin_port));

        printf("client say : %s\n", recvbuf);

        // 发送数据
        sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

    }

    close(fd);
    return 0;

UDP客户端的设置

无需绑定端口,开放接收,仅在发送的时候需要提供服务端ip


    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 服务器的地址信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);

    int num = 0;
    // 3.通信
    while(1) {

        // 发送数据
        char sendBuf[128];
        sprintf(sendBuf, "hello , i am client %d \n", num++);
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));

        // 接收数据
        int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
        printf("server say : %s\n", sendBuf);

        sleep(1);
    }

    close(fd);
    return 0;

广播

仅可用于局域网
服务端须设置socket属性:

 // 2.设置广播属性
 int op = 1;
 setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));
 
 // 3.创建一个广播的地址
 struct sockaddr_in cliaddr;
 cliaddr.sin_family = AF_INET;
 cliaddr.sin_port = htons(9999);
 inet_pton(AF_INET, "192.168.193.255", &cliaddr.sin_addr.s_addr);

客户端需绑定端口,ip是可选的:INADDR_ANY

// 2.客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));

组播

需要接收信息的客户端可以加入多播组进行接收
可用于广域网和局域网

服务端需要设置的:
需要设置自身的多播属性(指定发送端口,其实可以不设置)和组ip,然后向组发送数据

// 2.设置多播的属性,设置外出接口
struct in_addr imr_multiaddr;
// 初始化多播地址
inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));

// 3.初始化客户端的地址信息
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr);

sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

客户端的设置:
通过加入多播组来收取消息

    // 4. 初始化多播组结构体
    mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_ADDR);
    mreq.imr_interface.s_addr = INADDR_ANY;

    // 5. 加入多播组
    if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
        perror("setsockopt: IP_ADD_MEMBERSHIP");
        close(sockfd);
        return -1;
    }

多播组结构体:

struct ip_mreq {
    struct in_addr imr_multiaddr; /* 多播组的 IP 地址 */
    struct in_addr imr_interface; /* 用于接收多播数据的本地网络接口的 IP 地址 */
};
  • mr_multiaddr:这是一个 struct in_addr 类型的成员,用于指定要加入或离开的多播组的 IP 地址。多播组的 IP 地址属于 D 类地址,范围是 224.0.0.0 - 239.255.255.255。
  • imr_interface:同样是 struct in_addr 类型,它指定了用于接收多播数据的本地网络接口的 IP 地址。若将其设置为 INADDR_ANY(值为 0.0.0.0),表示使用任意可用的网络接口来接收多播数据。

本地套接字

实现进程间通信
通过对两个文件路径的读写进行通信

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值