1.介绍
UDP(User Datagram Protocol,用户数据报协议)是在一组互连的计算机网络环境中提供分组交换计算机通信的数据报模式。该协议假定使用IP作为底层协议,按照OSI模型工作在传输层。UDP为应用程序提供了一种以最少的协议机制向其他程序发送消息的过程 。该协议是面向事务的,不保证传递和重复保护。需要有序、可靠地传输数据流的应用程序应使用传输控制协议 (TCP)。
2.实现
对于使用UPD进行网络编程多的不说直接上代码来实现一个简单的基于udp的echo server。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main() {
int sfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sfd < 0) {
return -1;
}
struct sockaddr_in s_addr;
s_addr.sin_family = PF_INET;
s_addr.sin_port = htons(9999);
s_addr.sin_addr.s_addr = INADDR_ANY;
socklen_t slen = sizeof(s_addr);
if(bind(sfd, (struct sockaddr*)&s_addr, sizeof(struct sockaddr)) < 0) {
close(sfd);
return -1;
}
printf("udp_server start...\n");
while(1) {
char buffer[1024] = {0};
struct sockaddr_in c_addr;
socklen_t clen = sizeof(c_addr);
int nread = recvfrom(sfd, buffer, 1024, 0, (struct sockaddr*)&c_addr, &clen);
if(nread <= 0) {
continue;
}
printf("recv: %s , cread = %d\n", buffer, nread);
printf("Data received from %s:%d len = %d\n", inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port), clen);
sendto(sfd, buffer, nread, 0, (struct sockaddr*)&c_addr, clen);
}
close(sfd);
return 0;
}
下面在实现一个client与server配合:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
int main(int argc, char** argv) {
if(argc < 3) {
printf("program [server_ip] [server_port] [client_port]\n");
return -1;
}
int sfd;
struct sockaddr_in server_addr, client_addr;
char buffer[1024];
int nread;
// 创建 UDP 套接字
sfd = socket(PF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (sfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置server地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = PF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = PF_INET;
client_addr.sin_port = htons(atoi(argv[3]));
client_addr.sin_addr.s_addr = INADDR_ANY;
socklen_t clen = sizeof(client_addr);
socklen_t slen = sizeof(server_addr);
if(bind(sfd, (struct sockaddr*)&client_addr, sizeof(struct sockaddr))) {
perror("chid bind");
exit(1);
}
while (1) {
// 从用户输入读取数据
printf("Enter message to send: ");
fgets(buffer, 1024, stdin);
buffer[strcspn(buffer, "\n")] = '\0'; // 去掉换行符
// 发送数据到服务器
if (sendto(sfd, buffer, strlen(buffer), 0, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)) == -1) {
perror("sendto");
continue;
}
// 接收服务器返回的数据
nread = recvfrom(sfd, buffer, 1024, 0, (struct sockaddr*)&server_addr, &slen);
if (nread == -1) {
perror("recvfrom");
continue;
}
buffer[nread] = '\0';
printf("Received echo: %s\n", buffer);
}
close(sfd);
return 0;
}
现在测试一下,看看结果:
OK,看起来很完美,server两台client连接并且成功接受和发送数据,但是我们知道UDP是基于报文的通信协议,它的协议本身是没有连接的概念的,竟然没有连接又何谈并发呢?这个问题我们先保留,我们看下一个问题,在上述server代码中对于简单的echo确实没有问题,但是UDP是不可靠的协议,他不保证数据一定会送到,如果要引入复杂的业务处理或者对应的可靠机制,那对于编码来说简直就是灾难,对于后续维护来说更是灾难中的灾难,那么我们怎么才能写出优雅的UDP server代码呢?我们又如何UDP的并发编程呢?
答案也很简单,我们模仿TCP,TCP的机制无需多言,UDP竟然协议层没有连接的概念,那么我们就应用层来加,那么又会有问题,我们为什么不直接用TCP?你这不是脱裤子放屁,多此一举吗?理由也很简单,TCP内部的东西你改不了,但UDP可以随意捏成我们想要的形状。
现在我们来对服务端加入连接的概念,具体思路就是绑定一个9999端口作为单独用来监听client发来的握手包,以此来创建一个连接,便于后续通信,先实现一个udp的accept函数,大体思路如下:
int udp_accept(int sd, struct sockaddr_in my_addr)
{
int new_fd = -1;
int ret = 0;
int reuse = 1;
char buf[16];
struct sockaddr_in peer_addr;
socklen_t cli_len = sizeof(peer_addr);
//1.接受数据,拿到client信息
ret = recvfrom(sd, buf, 16, 0, (struct sockaddr *)&peer_addr, &cli_len);
if (ret < 0) {
return -1;
}
//2.创建一个新的fd与client连接
if ((new_fd = socket(PF_INET, SOCK_DGRAM, 0)) == -1) {
return -1;
}
//3.绑定
ret = bind(new_fd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr));
if (ret){
close(new_fd);
return -1;
}
//4.调用connect设置默认目标地址
peer_addr.sin_family = PF_INET;
if (connect(new_fd, (struct sockaddr *) &peer_addr, sizeof(struct sockaddr)) == -1) {
close(new_fd);
return -1;
}
//5.回复client,告诉他我们的新端口
char buffer[16] = "hello client";
sendto(new_fd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer_addr, cli_len);
return new_fd;
}
有了连接函数,我们再使用epoll来检测listenfd的触发和后续client的时间,从而实现一个基于UDP的reator并发模型。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
typedef int (*rcallback)(int sd);
typedef int (*wcallback)(int sd);
typedef int (*acallback)(int sd);
typedef struct conn {
int fd;
union {
rcallback _rcall;
acallback _acall;
} rcall;
wcallback wcall;
char rbuffer[1024];
char wbuffer[1024];
struct sockaddr_in saddr;
struct sockaddr_in caddr;
socklen_t slen;
socklen_t clen;
int rlen;
int wlen;
} conn;
conn conn_list[2048] = {0};
int epfd = 0;
int connect_port = 10000;
void mod_event(int fd, int event) {
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
int read_callback(int sd) {
conn* c = &conn_list[sd];
c->rlen = recvfrom(sd, c->rbuffer, 1024, 0, NULL, NULL);
mod_event(sd, EPOLLOUT);
return c->rlen;
}
int write_callback(int sd) {
conn* c = &conn_list[sd];
memcpy(c->wbuffer, c->rbuffer, c->rlen);
c->wlen = c->rlen;
c->wlen = sendto(sd, c->wbuffer, c->wlen, 0, (struct sockaddr*)&c->caddr, c->clen);
mod_event(sd, EPOLLIN);
return c->wlen;
}
int udp_accept(int sd)
{
int new_fd = -1;
int ret = 0;
int reuse = 1;
char buf[16];
struct sockaddr_in peer_addr;
socklen_t cli_len = sizeof(peer_addr);
//1.拿到本地信息,然后改一个端口,于client建立另外一条连接
struct sockaddr_in my_addr = conn_list[sd].saddr;
my_addr.sin_port = htons(connect_port++);
//2.接受client的握手数据
ret = recvfrom(sd, buf, 16, 0, (struct sockaddr *)&peer_addr, &cli_len);
if (ret < 0) {
return -1;
}
//3.创建一个新的fd
if ((new_fd = socket(PF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)) == -1) {
return -1;
}
int opt = 1;
if(setsockopt(new_fd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt))){
exit(1);
}
if(setsockopt(new_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse))){
exit(1);
}
//4.与新端口绑定
ret = bind(new_fd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
if (ret){
close(new_fd);
return -1;
}
//5.连接,设置默认目标地址
peer_addr.sin_family = PF_INET;
if (connect(new_fd, (struct sockaddr *) &peer_addr, sizeof(struct sockaddr)) == -1) {
close(new_fd);
return -1;
}
//6.回应client,让对端知道我们的新端口
char buffer[16] = "hello client";
sendto(new_fd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer_addr, sizeof(struct sockaddr_in));
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = new_fd;
//7.加入epoll
if(epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &ev) < 0) {
close(new_fd);
return -1;
}
conn_list[new_fd].fd = new_fd;
conn_list[new_fd].rcall._acall = read_callback;
conn_list[new_fd].wcall = write_callback;
conn_list[new_fd].saddr = my_addr;
conn_list[new_fd].slen = sizeof(my_addr);
conn_list[new_fd].caddr = peer_addr;
conn_list[new_fd].clen = sizeof(peer_addr);
return new_fd;
}
int main() {
int sfd = socket(PF_INET, SOCK_DGRAM, 0);
if (sfd < 0) {
return -1;
}
struct sockaddr_in s_addr;
s_addr.sin_family = PF_INET;
s_addr.sin_port = htons(9999);
s_addr.sin_addr.s_addr = INADDR_ANY;
socklen_t slen = sizeof(s_addr);
if(bind(sfd, (struct sockaddr*)&s_addr, sizeof(struct sockaddr)) < 0) {
close(sfd);
return -1;
}
epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sfd;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev) < 0) {
close(sfd);
close(epfd);
return -1;
}
conn_list[sfd].fd = sfd;
conn_list[sfd].rcall._acall = udp_accept;
conn_list[sfd].wcall = NULL;
conn_list[sfd].saddr = s_addr;
conn_list[sfd].slen = sizeof(s_addr);
printf("udp_server start...\n");
while(1) {
struct epoll_event evs[128] = {0};
int nready = epoll_wait(epfd, evs, 128, -1);
if(nready > 0) {
for(int i = 0; i < nready; ++i) {
int sd = evs[i].data.fd;
if(evs[i].events & EPOLLIN) {
if(conn_list[sd].rcall._rcall) {
conn_list[sd].rcall._rcall(sd);
}
}
if(evs[i].events & EPOLLOUT) {
if(conn_list[sd].wcall) {
conn_list[sd].wcall(sd);
}
}
}
}
}
close(sfd);
close(epfd);
return 0;
}
ok,接下来实现client端:
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <strings.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#define SO_REUSEPORT 15
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)
void createClient(int id,int myPort,int peerPort){
int socketFd;
struct sockaddr_in peer_Addr;
peer_Addr.sin_family = PF_INET;
peer_Addr.sin_port = htons(9999);
peer_Addr.sin_addr.s_addr = inet_addr("192.168.126.128");
struct sockaddr_in self_Addr;
self_Addr.sin_family = PF_INET;
self_Addr.sin_port = htons(10000);
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 = 1;
if(setsockopt(socketFd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt))){
exit(1);
}
if(setsockopt(socketFd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt))){
exit(1);
}
if (bind(socketFd, (struct sockaddr *) &self_Addr, sizeof(struct sockaddr))){
perror("chid bind");
exit(1);
}
char buffer[1024] = {0};
memset(buffer, 0, 1024);
sprintf(buffer, "hello server %d", 0);
sendto(socketFd, buffer, strlen(buffer), 0, (struct sockaddr *) &peer_Addr, sizeof(struct sockaddr_in));
bzero(&peer_Addr, sizeof(peer_Addr));
char rbuffer[1024] = {0};
socklen_t slen = sizeof(peer_Addr);
// 接收服务器返回的数据
int nread = recvfrom(socketFd, rbuffer, 1024, 0, (struct sockaddr*)&peer_Addr, &slen);
if (nread == -1) {
close(socketFd);
}
peer_Addr.sin_family = PF_INET;
if(connect(socketFd, (struct sockaddr *) &peer_Addr, sizeof(struct sockaddr)) == -1) {
perror("chid connect");
exit(1);
}
memset(buffer, 0, 1024);
memset(rbuffer, 0, 1024);
strcpy(buffer, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
sendto(socketFd, buffer, strlen(buffer), 0, (struct sockaddr *) &peer_Addr, sizeof(struct sockaddr_in));
recvfrom(socketFd, rbuffer, 1024, 0, (struct sockaddr*)&peer_Addr, &slen);
}
void serial(int clinetNum){
for(int i=1;i<=clinetNum;i++){
createClient(i,30000+i,9999);
}
}
int main(int argc, char * argv[])
{
struct timeval tv_begin;
gettimeofday(&tv_begin, NULL);
serial(1024);
struct timeval tv_end;
gettimeofday(&tv_end, NULL);
int time_used = TIME_SUB_MS(tv_end, tv_begin);
printf("success time_used: %d\n", time_used);
return 0;
}
运行看结果 :
3.总结
综上,我们模仿tcp的三次握手流程,构建了一个基于UDP的连接模型,当然,上面的代码只有两次握手,这个后面在继续优化。
最后再来总结一下,整个udp server的缺陷:
1.目前没有应答机制,再加上用的是阻塞IO,也就是说一旦udp丢包,我们整个流程就走不下去,server端还好只是不会触发IO事件,client端会在recvfrom函数内阻塞。
2. 可以很明显的看到,服务端的连接fd不会被回收,因为我们不知道client什么时候回断开,就算client端断开了连接我们也不知道,这种情况下资源总会有耗尽的时候。
3.目前的连接数没办法超过了最大端口数。
针对上述缺陷,后续的优化方向是引入kcp和加入心跳包机制以及重复利用端口,也就是说server端一个端口可能会同时维护多个udp client。
学习参考: