udp通信过程
一、客户端
1. 初始化本地sockaddr_in,初始化对端sockaddr_in
struct sockaddr_in
{
addr:
port:
}
- 创建本地的 套接字
fd = socket(PF_INET, SOCK_DGRAM, 0)
// 注意这里 的二个参数是SOCK_DGRAM 如果是TCP套接字的话 第二个参数是SOCK_STREAM
这里可以设置端口和地址复用(多个套接字绑定到同一个地址和端口
if(setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &reuse,sizeof(reuse))){
exit(1);
}
if(setsockopt(socketFd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse))){
exit(1);
}
在服务器重启时,快速重新绑定到同一个端口,而无需等待 TCP 的 TIME_WAIT 状态。
服务器端允许多个进程或线程绑定到同一个 UDP 端口,从而实现负载均衡。
绑定 本地地址和端口:
bind(socketFd, (struct sockaddr *) &self_Addr, sizeof(struct sockaddr))
连接 :绑定创建本地套接字 和 对端地址和端口 ,可以不绑定但是第一次发送时会自己选择一个port
connect(socketFd, (struct sockaddr *) &peer_Addr, sizeof(struct sockaddr))
connect之后可以调用send()直接发送,
不然的话调用sendto发送 sendto参数中
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/resource.h>
void createClient(int id,int myPort,int peerPort){
int reuse = 1;
int socketFd;
struct sockaddr_in peer_Addr;
peer_Addr.sin_family = PF_INET;
peer_Addr.sin_port = htons(peerPort);
peer_Addr.sin_addr.s_addr = inet_addr("192.168.1.136");
struct sockaddr_in self_Addr;
self_Addr.sin_family = PF_INET;
self_Addr.sin_port = htons(myPort);
self_Addr.sin_addr.s_addr = inet_addr("0.0.0.0");
if ((socketFd = socket(PF_INET, SOCK_DGRAM| SOCK_CLOEXEC, 0)) == -1) {
perror("child socket");
exit(1);
}
int opt=fcntl(socketFd,F_GETFL);
fcntl(socketFd,F_SETFL,opt|O_NONBLOCK);
if(setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &reuse,sizeof(reuse))){
exit(1);
}
if(setsockopt(socketFd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse))){
exit(1);
}
if (bind(socketFd, (struct sockaddr *) &self_Addr, sizeof(struct sockaddr))){
perror("chid bind");
exit(1);
} else {
}
if (connect(socketFd, (struct sockaddr *) &peer_Addr, sizeof(struct sockaddr)) == -1) {
perror("chid connect");
exit(1);
}
usleep(1); // --> key
char buffer[1024] = {0};
memset(buffer, 0, 1024);
sprintf(buffer, "hello %d", id);
sendto(socketFd, buffer, strlen(buffer), 0, (struct sockaddr *) &peer_Addr, sizeof(struct sockaddr_in));
}
void serial(int clinetNum){
for(int i=1;i<=clinetNum;i++){
createClient(i,2025+i,1234);
}
}
int main(int argc, char * argv[])
{
serial(1000);
printf("serial success\n");
return 0;
}
二、服务端
1、创建本地套接字listenfd
2、bind本地addr
3、创建epoll对象,将 listenfd 加入该epoll对象中
4、通过epoll_wait() 检测事件,如果第一次发消息,该检测到事件的fd一定是listenfd
5、接收处理,然后服务端获取客户端的连接的ip 和port ,
重新建立一个新连接,设置端口复用,通过connect将新的socket和客户端地址端口绑定(这样下次该客户端发送消息时,内核会将事件与该fd绑定在一起)
6、然后将该socket加入epoll实例中,通过epoll_wait()来检测读写事件
当该客户端再次发送消息时,会检测到是刚刚新建的fd触发的事件,根据该fd进行解析
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <pthread.h>
#include <assert.h>
#define SO_REUSEPORT 15
#define MAXBUF 10240
#define MAXEPOLLSIZE 100
int flag = 0;
int count = 0;
int read_data(int sd)
{
char recvbuf[MAXBUF + 1];
int ret;
struct sockaddr_in client_addr;
socklen_t cli_len=sizeof(client_addr);
bzero(recvbuf, MAXBUF + 1);
ret = recvfrom(sd, recvbuf, MAXBUF, 0, (struct sockaddr *)&client_addr, &cli_len);
if (ret > 0) {
printf("read[%d]: %s from %d\n", ret, recvbuf, sd);
} else {
printf("read err:%s %d\n", strerror(errno), ret);
}
//fflush(stdout);
}
int udp_accept(int sd, struct sockaddr_in my_addr)
{
int new_sd = -1;
int ret = 0;
int reuse = 1;
char buf[16];
struct sockaddr_in peer_addr;
socklen_t cli_len = sizeof(peer_addr);
ret = recvfrom(sd, buf, 16, 0, (struct sockaddr *)&peer_addr, &cli_len);
if (ret < 0) {
return -1;
}
// printf("ret: %d, buf: %s\n", ret, buf);
if ((new_sd = socket(PF_INET, SOCK_DGRAM, 0)) == -1) {
perror("child socket");
exit(1);
} else {
printf("%d, parent:%d new:%d\n",count++, sd, new_sd); //1023
}
ret = setsockopt(new_sd, SOL_SOCKET, SO_REUSEADDR, &reuse,sizeof(reuse));
if (ret) {
exit(1);
}
ret = setsockopt(new_sd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));
if (ret) {
exit(1);
}
//my_addr.sin_port += count;
ret = bind(new_sd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr));
if (ret){
perror("chid bind");
exit(1);
} else {
}
peer_addr.sin_family = PF_INET;
//printf("aaa:%s\n", inet_ntoa(peer_addr.sin_addr));
if (connect(new_sd, (struct sockaddr *) &peer_addr, sizeof(struct sockaddr)) == -1) {
perror("chid connect");
exit(1);
} else {
}
out:
return new_sd;
}
int main(int argc, char **argv)
{
int listener, kdpfd, nfds, n, curfds;
socklen_t len;
struct sockaddr_in my_addr, their_addr;
unsigned int port;
struct epoll_event ev;
struct epoll_event events[MAXEPOLLSIZE];
int opt = 1;;
int ret = 0;
port = 1234;
if ((listener = socket(PF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
} else {
printf("socket OK\n");
}
ret = setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if (ret) {
exit(1);
}
ret = setsockopt(listener, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
if (ret) {
exit(1);
}
int flags = fcntl(listener, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(listener, F_SETFL, flags);
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = PF_INET;
my_addr.sin_port = htons(port);
my_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) {
perror("bind");
exit(1);
} else {
printf("IP bind OK\n");
}
kdpfd = epoll_create(MAXEPOLLSIZE);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listener;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0) {
fprintf(stderr, "epoll set insertion error: fd=%dn", listener);
return -1;
} else {
printf("ep add OK\n");
}
while (1) {
nfds = epoll_wait(kdpfd, events, MAXEPOLLSIZE, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listener) {
int new_sd;
struct epoll_event child_ev;
while (1) {
new_sd = udp_accept(listener, my_addr);
if (new_sd == -1) break;
child_ev.events = EPOLLIN;
child_ev.data.fd = new_sd;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_sd, &child_ev) < 0) {
fprintf(stderr, "epoll set insertion error: fd=%dn", new_sd);
return -1;
}
}
} else {
read_data(events[n].data.fd);
}
}
}
close(listener);
return 0;
}
三、整个流程梳理:
1、客户端发送消息
客户端通过sendto向服务端listenfd发送消息
服务端的listenfd通过epoll_wait()检测到可读事件,触发udp_accept函数
2、服务端创建新的socket
udp_accept函数的核心逻辑:
1、接收客户端地址:通过recvfrom获取客户端ip和port
2、创建新的socket:生成一个新的udp socket;
3、绑定相同端口: 新的udp socket绑定到相同的服务端地址(ip + port),依赖reuse实现绑定同一个端口
4、将新的socket和客户端地址绑定 connect(newfd, &peer_port);
5、将新的socket添加到epoll实例,后续通过它来接受消息
3、后续通信流程
客户端继续发送消息:数据包的目标端口号仍未1234
内核路由规则:由于新的socket以通过connect绑定到客户端地址,于是内核会将该客户端的后续数据包自动路由到新的socket;
主socket(listenfd)保持监听: 主socket继续接收其他客户端的首次消息,重复上述流程。