同步UDP请求数据的问题
不管是请求DNS资源还是其他资源。如果以串行的方式请求数据,也就是send以后recv阻塞等待获取数据,这样做的效率非常低效,网络延迟、服务器处理请求、再加上recv从内核态拷贝到用户态,这段时间还是比较长的。
接着我们考虑解决办法:
- 主线程send完成以后,创建一个子线程,或者使用线程池,让子线程来负责recv。这确实是一个不错的解决办法,但如果服务器处理数据的时间很长,那子线程还是会被recv阻塞住,并且如果send发送的请求很多,会出现线程池不够用,或者频繁创建线程的情况。
- 主线程send完成以后,将其加入epoll中等待,当有事件发生时再recv。这个方法相比于第一种使用了更少的线程资源,但本质上还是同步读取的过程。
实际上,我们可以采取主线程send发送,然后创建一个子线程,让子线程帮我们epoll_wait等待读取,这样主线程发送完就可以去执行别的任务,而不需要同步去recv读取请求的数据。
异步请求的模型
要实现异步处理主要分为四个步骤:
- init
调用epoll_create创建一个句柄,然后调用pthread_create创建一个子线程执行回调函数,从而实现异步处理。 - commit
建立网络连接,返回对应的sock,然后组织好对应的协议(比如DNS,也就是组织好对应要发送的数据),发送到对应的服务器,将sock加入到epoll中。 - callback
设置回调函数,让第一步的子线程执行,回调函数主要负责epoll_wait第二步所有的sock,当有事件发生时,就recv接收数据,然后对得到的数据进行操作。 - close
当对应的sock不再需要时,就在回调函数中close它们。
具体的代码
以客户端给服务器发送数据,然后服务器返回对应的数据为例:
服务器代码:
服务器并不是重点,怎么实现都行。
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sstream>
#include <stdlib.h>
#include <sys/epoll.h>
using namespace std;
struct ep_arg
{
int sockfd;
};
class udpServer
{
private:
// std::string ip;
int port; //端口号
int sock; //套接字
int epfd;
public:
// 127.0.0.1本地环回,8080默认端口号
//通常用来进行网络代码的本地测试
udpServer(int _port = 9999)
: port(_port)
{
}
void initServer()
{
// socket的返回值为文件描述符,类似于open函数
sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
//转换ip
// local.sin_addr.s_addr=inet_addr(ip.c_str());
// INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP
local.sin_addr.s_addr = INADDR_ANY;
//绑定套接字,ip地址和端口号
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error!\n"
<< std::endl;
exit(1);
}
epfd = epoll_create(1); //
if (epfd < 0)
{
cout << "epoll_create失败" << endl;
}
struct epoll_event ev;
ev.data.fd = sock;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
}
static void *callback(void *ptr)
{
int sock = *(int *)ptr;
char msg[64];
msg[0] = '\0';
struct sockaddr_in end_point;
socklen_t len = sizeof(end_point);
ssize_t s = recvfrom(sock, msg, sizeof(msg) - 1, 0, (struct sockaddr *)&end_point, &len);
if (s > 0)
{
//拿到发送端的ip地址,将整形四字节转成点分十进制的字符串
std::string cli = inet_ntoa(end_point.sin_addr);
cli += ":";
//拿到发送端的端口号
//要将网络字节序转为主机字节序,并且再转成字符串
cli += std::to_string(ntohs(end_point.sin_port));
msg[s] = '\0';
std::cout << cli << "#" << msg << std::endl;
//再给客户端发消息
std::string echo_string = msg;
echo_string += "[server echo!]";
//发之前休眠1秒,模拟网络延迟
sleep(1);
sendto(sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&end_point, len);
}
}
void start()
{
while (1)
{
struct epoll_event events[128] = {0};
int nready = epoll_wait(epfd, events, 128, -1);
if (nready < 0)
{
if (errno == EINTR || errno == EAGAIN)
{
continue;
}
else
{
break;
}
}
else if (nready == 0)
{
continue;
}
int i = 0;
for (i = 0; i < nready; i++)
{
pthread_t thread_id;
int ret = pthread_create(&thread_id, NULL, callback, &events[i].data.fd);
}
}
}
~udpServer()
{
close(sock);
}
};
int main(int argc, char *argv[])
{
//绑定端口号
udpServer *up = new udpServer(atoi("9999"));
up->initServer(); //将服务器初始化
up->start();
delete up;
return 0;
}
客户端代码:
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/epoll.h>
using namespace std;
class udpClient
{
public:
std::string ip;
int port; //端口号
int sock;
int epfd;
public:
static void *callback(void *ptr)
{
int epfd = *(int *)ptr;
while (1)
{
struct epoll_event events[128] = {0};
int nready = epoll_wait(epfd, events, 128, -1);
if (nready < 0)
{
if (errno == EINTR || errno == EAGAIN)
{
continue;
}
else
{
break;
}
}
else if (nready == 0)
{
continue;
}
int i = 0;
for (i = 0; i < nready; i++)
{
int sockfd = events[i].data.fd;
char buffer[1024] = {0};
struct sockaddr_in addr;
size_t addr_len = sizeof(struct sockaddr_in);
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);
std::cout << "server#" << buffer << std::endl;
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
printf("epoll_ctl DEL --> sockfd:%d\n", sockfd);
close(sockfd);
}
}
}
void initEpoll()
{
epfd = epoll_create(1); //
if (epfd < 0)
{
cout << "epoll_create失败" << endl;
}
cout << epfd << endl;
pthread_t thread_id;
int ret = pthread_create(&thread_id, NULL, callback, &epfd);
if (ret)
{
perror("pthread_create");
return;
}
usleep(1); // 子线程先走
cout << "epoll_create成功" << endl;
}
//连服务器的ip和端口号
udpClient(std::string _ip = "127.0.0.1", int _port = 9999)
: ip(_ip), port(_port)
{
}
void start()
{
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str());
for (int i = 0; i < 500; i++)
{
std::string msg;
//创建socket,客户端不需要绑定
sock = socket(AF_INET, SOCK_DGRAM, 0);
msg.resize(5, 'A' + i % 50);
sendto(sock, msg.c_str(), msg.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
char echo[128];
//接收服务端发回来的消息
struct epoll_event ev;
ev.data.fd = sock;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
// char buffer[1024] = {0};
// struct sockaddr_in addr;
// size_t addr_len = sizeof(struct sockaddr_in);
// int n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&addr, (socklen_t *)&addr_len);
// std::cout << "server#" << buffer << std::endl;
}
}
~udpClient()
{
close(sock);
}
};
void Usage(std::string proc)
{
std::cout << "Usage:" << proc << "server_ip server_port" << std::endl;
}
//通过参数列表获取输入的ip和端口号,该ip和端口号就是服务器的ip和端口号
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
//绑定ip地址和端口号,端口号是一个整数,要进行强转
udpClient uc(argv[1], atoi(argv[2]));
uc.initEpoll();
uc.start();
while (1)
{
sleep(1);
cout << "主线程执行其他任务" << endl;
}
return 0;
}
具体的效果:

由于客户端采用了异步的方式,相比于原来的串行,即使传输和接收有延迟,也能够很快接收出来,并且不影响主线程执行其他任务。
1740

被折叠的 条评论
为什么被折叠?



