文章目录
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 通信的流程
服务器端 (被动接受连接的角色)
- 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
- 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
- 设置监听,监听的fd开始工作
- 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
- 通信
- 接收数据
- 发送数据
- 通信结束,断开连接
客户端(主动创建链接)
- 创建一个用于通信的套接字(fd)
- 连接服务器,需要指定连接的服务器的 IP 和 端口
- 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
- 通信结束,断开连接
常用函数
#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 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
- 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用
进程都调用了 close,套接字将被释放。 - 在多进程中如果一个进程调用了 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
在网络编程里,send
、sendto
、recv
、recvfrom
是用于数据收发的关键函数。下面针对不同的编程语言和网络编程模型,详细阐述它们的用法。
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
:发送方地址的长度。
总结
send
和recv
适用于面向连接的套接字(TCP),在使用前需要先建立连接。sendto
和recvfrom
适用于无连接的套接字(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),表示使用任意可用的网络接口来接收多播数据。
本地套接字
实现进程间通信
通过对两个文件路径的读写进行通信