前面已经介绍了网络套接字的创建和绑定,这篇文章会通过UDP套接字实现一个UDP服务器。
先介绍将使用的接口。
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
第一个表示从哪个套接字获取信息,第二个是缓冲区,len是缓冲区长度,数据就会被读到缓冲区中,flags可直接填0。
src_addr是输出型参数,将会返回是哪个客户端发送的信息,addrlen指向src_addr结构的结构体大小,若这两个参数为NULL,则不获取客户端的相关信息。
使用这个函数时,需要自己定义sockaddr_in结构体,并且定义一个变量为sockaddr_in的大小。
然后将其传入,以获得客户端的IP地址和端口。
返回值为实际收到的字节数,出现错误则返回-1。
sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd是服务器绑定的套接字的文件描述符,buf是你将要发送的信息,len为buf的长度,flags设为0, dest_addr表示要发消息给谁,addrlen是传入的结构体的长度。
返回值为发送的字节数,出现错误则返回-1。
服务器代码
现在已经可以实现一个UDP服务器了
#include <iostream>
#include <string>
#include <sys/types.h>
#include <cstdlib>
#include <sys/socket.h>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <functional>
//服务器将绑定的IP地址
const std::string IP = "0.0.0.0";
//缓冲区的大小
const int MAXBUFSIZE = 1024;
//服务器对发送过来的信息进行何种处理后返回处理后的信息
using func_t = std::function<std::string(const std::string&)>;
class server
{
public:
//创建并绑定套接字
server(uint16_t port, func_t handle, std::string ip = IP);
//服务器运行,并且接收信息,向客户端返回处理后的信息
void run();
private:
//服务器如何处理收到的信息,由程序员自己定义
func_t _handle;
//打开的套接字的文件描述符
int _sock_fd;
//服务器是否运行
bool running;
//服务器绑定的端口
uint16_t _port;
//服务器绑定的IP地址
std::string _ip;
};
这里解释一下0.0.0.0IP地址,它意味着服务器绑定本机上的所有网络接口,一般服务器主机会有不止一块网卡,需要将所有网络接口都绑定到服务器上,而不是某一个IP地址。
下面给出服务器初始化的代码
server(uint16_t port, func_t handle, std::string ip = IP)
:_port(port),_ip(ip)
{
running = false;
_handle = handle;
//创建UDP套接字
_sock_fd = socket(AF_INET,SOCK_DGRAM,0);
if(_sock_fd < 0)
{
std::cout<<"socket error"<<std::endl;
exit(1);
}
//绑定套接字
sockaddr_in svr_addr;
svr_addr.sin_family = AF_INET;
//将端口由主机字节序转为网络字节序
svr_addr.sin_port = htons(_port);
//将字符串风格的IP地址转为网络字节序的32位
svr_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
socklen_t len = sizeof(svr_addr);
//由于bind的参数是sockaddr*类型,这里需要强转
int err = bind(_sock_fd, (sockaddr*)&svr_addr, len);
if(err != 0)
{
std::cout<<"bind error"<<std::endl;
exit(1);
}
}
然后是服务器运行的代码
void run()
{
running = true;
//保存客户端的IP地址和端口号,以便于将处理后的信息发给他
sockaddr_in client_addr;
//用于接收数据的缓冲区
char buf[MAXBUFSIZE];
std::cout << "Server running..." << std::endl;
while(running)
{
memset(buf, 0, MAXBUFSIZE);
socklen_t len = sizeof(client_addr);
ssize_t recv_size = recvfrom(_sock_fd, buf, sizeof(buf) - 1,0,(sockaddr*)&client_addr, &len);
client_addr.sin_port = ntohs(client_addr.sin_port);
client_addr.sin_addr.s_addr = ntohl(client_addr.sin_addr.s_addr);
if(recv_size < 0)
{
std::cout<<"recv error"<<std::endl;
continue;
}
//程序员自定义的处理数据函数
std::string ret = _handle(std::string(buf));
std::cout << ret << std::endl;
memset(buf, 0, MAXBUFSIZE);
//向客户端发送处理后的信息
ssize_t send_size = sendto(_sock_fd, ret.c_str(), ret.size(), 0, (sockaddr*)&client_addr, len);
if(send_size < 0)
{
std::cout<<"send error"<<std::endl;
continue;
}
}
}
然后是main函数
//服务器程序像这样使用 ./server [port]
//server是我自己的可执行程序名称,你可以定义自己的
void Usage()
{
std::cout<< "Usage: " << "./server" << " port"<<std::endl;
}
//只是简单的将信息回显
std::string handle(const std::string& s)
{
return "Server recv:" + s;
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage();
return 1;
}
uint16_t port = atoi(argv[1]);
server svr(port, handle);
svr.run();
return 0;
}
结合前面的讲解,这些代码应该可以理解,如果还不理解,建议自己动手敲一遍,百分百能理解。
只有服务端还不够完整,不能直观的看到运行效果,下面再给出客户端代码
客户端代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>
const int MAXBUFSIZE = 1024;
class client
{
public:
client();
void SendTo(const sockaddr_in& server_addr, const std::string& msg);
private:
int _sock_fd;
};
客户端初始化
client()
{
_sock_fd = socket(AF_INET,SOCK_DGRAM,0);
if(_sock_fd < 0)
{
std::cerr<<"socket error"<<std::endl;
exit(1);
}
}
客户端发送消息
void SendTo(const sockaddr_in& server_addr, const std::string& msg)
{
int ret = sendto(_sock_fd, msg.c_str(), msg.size(), 0, (sockaddr*)&server_addr, sizeof(server_addr));
if(ret < 0)
{
std::cerr<<"sendto error"<<std::endl;
exit(2);
}
}
main函数
void Usage()
{
std::cout<< "Usage: " << "./server" << " port"<<std::endl;
}
std::string handle(const std::string& s)
{
return "Server recv:" + s;
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage();
return 1;
}
uint16_t port = atoi(argv[1]);
server svr(port, handle);
svr.run();
return 0;
}
有了服务器的编写经验,客户端代码应该很容易看懂,你也可以自己写一个运行在电脑上看效果。
你可以使用本地回环地址 127.0.0.1对代码进行测试,用法如下,假设端口是8080
./client 127.0.0.1 8080