Socket编程-udp
1. 前言
最先使用udp
进行socket
编程,最直接的原因就是因为udp
简单,方便我们快速熟悉socket
各种系统调用
我们一共会完成三份代码,第一份我们会实现两台主机之间的简单聊天系统;第二份在第一份的前提下,我们加上一个翻译的业务,当client
向server
发送一个英文单词,server
会给client
返回该单词的中文意思;第三份在第一份的前提下,实现简单的群聊系统
2. udp_echo_server
首先,使用socket
函数创建socket
文件,socket
函数返回一个文件描述符fd
,这里可以简单认为是打开网卡
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
# domain -> AF_INET
# type -> SOCK_DGRAM
# protocol -> 0
其次,我们知道服务器启动时一定要与某个端口号绑定,未来客户端要拿着服务器的IP地址和port访问服务器
使用bind
函数将服务器的IP地址和port绑定到内核当中
其中,绑定IP地址和port时需要将主机序列转为网络序列,这里有现成的接口直接使用
#include <sys/types.h
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); # 将16位的port主机序列转网络序列
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
# 1. 将字符串类型的ip地址转结构化类型
# 2. 将ip地址主机序列转网络序列
void Initialize()
{
// 1. 创建socket文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0)
{
LOG(FATAL, "create sockfd error");
exit(1);
}
LOG(DEBUG, "create sockfd success, sockfd:%d", _sockfd);
// 2.bind
struct sockaddr_in local;
socklen_t len = sizeof(local);
local.sin_family = AF_INET;
local.sin_port = htons(_localport);
local.sin_addr.s_addr = inet_addr(_localip.c_str());
int n = bind(_sockfd, (const struct sockaddr*)&local, len);
if(n < 0)
{
LOG(FATAL, "bind error");
exit(1);
}
LOG(DEBUG, "bind success");
}
服务器运行起来后,从sockfd
中读取数据,对于udp
,读写不能使用read/write
,应当使用recvfrom/sendto
函数;接收到客户端的消息后,我们将客户端发送的消息打印出来,再发送回去
#include <sys/types.h>
#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);
# flags -> 默认为0
# dest_addr -> 要发送的主机
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
# src_addr -> 谁发送,将来也要向对方发送
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024] = { 0 };
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "[client echo]#" << buffer << std::endl;
sendto(_sockfd, buffer, sizeof(buffer)-1, 0, (const struct sockaddr*)&peer, len);
}
else
{
break;
}
}
_isrunning = false;
}
对于客户端,第一步同样需要创建socket
文件,但与服务器不同,客户端也需要绑定IP地址和port到内核,但不需要显示绑定,当客户端第一次发送数据是,由OS自动绑定
这是因为服务器是每个公司所有的,由公司选择IP地址和port,但我们的手机、电脑上有很多软件的客户端,如果是绑定指定的port,其他软件的客户端也要绑定同一个port时会发生冲突;因此客户端的IP地址和port由OS系统随机绑定,后面会验证客户端确实绑定了
客户端拿着服务器的IP地址和port,向服务器发送数据,再等待服务器回应,将回应打印
这里就需要提前知道服务器的IP地址和port,所以,我们使用命令行参数的方式将服务器的IP地址和port交给客户端
而服务器我们指定IP地址和port
int main()
{
std::string ip = "127.0.0.1"; # 本地环回
uint16_t port = 8888;
std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(ip, port);
udp_server->Initialize();
udp_server->Start();
return 0;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "Usage:" << argv[0] << " serverip serverport" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cout << "create socket error";
exit(1);
}
std::cout << "create socket success" << std::endl;
// client需要绑定 ip和port,但不需要显示绑定
// 当第一次发送数据时,OS会自动绑定
struct sockaddr_in server;
socklen_t len = sizeof(server);
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while(true)
{
std::string line;
std::cout << "please enter->";
getline(std::cin, line);
ssize_t n = sendto(sockfd, line.c_str(), line.size(), 0, (const struct sockaddr*)&server, len);
if(n > 0)
{
char buffer[1024] = { 0 };
struct sockaddr_in temp;
socklen_t length = sizeof(temp);
ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &length);
if(m > 0)
{
buffer[m] = '\0';
std::cout << buffer << std::endl;
}
else
{
break;
}
}
}
close(sockfd);
return 0;
}
现在我们来通信试试
上面代码中服务器绑定的是本地环回的IP地址(127.0.0.1),数据经过网络协议栈发送给自身主机,在向上交付;如果是绑定自身云服务器的公网IP地址呢?
经过测试,我们绑定失败,需要注意的是,不推荐在云服务器下自身的公网IP地址,并且,也不推荐服务器绑定一个指定的IP地址,因为服务器可能有多张网卡,就有多个IP地址,如果只指定一个IP地址绑定,那么就只会收到指定IP地址发送的数据;因此服务器的IP地址建议设置为INADDR_ANY
,表示绑定了任意IP,这样只要是发送到该主机下的数据就都能收到
我们再将服务器稍加修改,也使用命令行参数的方式绑定port
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "Usage:" << argv[0] << " port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(port);
udp_server->Initialize();
udp_server->Start();
return 0;
}
我们再来证明客户端确实是由OS帮我们绑定了IP地址和port
服务器一定收到了客户端的信息
char *inet_ntoa(struct in_addr in); # 将网络序列的结构化的ip地址转成字符串类型的ip地址
uint16_t ntohs(uint16_t netshort); # 将port由网络序列转成主机序列
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024] = { 0 };
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
buffer[n] = '\0';
InetAddr addr(peer);
std::cout << addr.AddrStr().c_str() << buffer << std::endl;
sendto(_sockfd, buffer, sizeof(buffer)-1, 0, (const struct sockaddr*)&peer, len);
}
else
{
break;
}
}
_isrunning = false;
}
为了方便IP地址port的打印,将IP地址和port封装
class InetAddr
{
protected:
void ToHost()
{
_ip = inet_ntoa(_addr.sin_addr);
_port = ntohs(_addr.sin_port);
}
public:
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
ToHost();
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
std::string AddrStr()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
~InetAddr()
{
}
protected:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
这样,我们的第一份udp_echo_server
代码就完成了
完整代码:[Practice/Lesson1/1. udp_echo_server · baiyahua/Linux - 码云 - 开源中国 (gitee.com)](https://gitee.com/baiyahua/linux/tree/master/Practice/Lesson1/1. udp_echo_server)
3. dict_server
现在,我们想在上面的代码上加上翻译业务,当客户端给服务器输入一个英文单词,服务器给客户端返回该英文单词的中文
同时,我们希望udp
服务器只负责读取和发送数据,也就是IO,而业务逻辑交给另外的模块处理,做到IO逻辑与业务逻辑解耦
这就需要我们将处理英文单词的方法以函数指针的方式交给udp
服务器,当udp
服务器收到英文单词时,回调该方法,将结果再发送回客户端
using service_t = std::function<std::string(const std::string&)>;
// ....
class UdpServer
{
// ...
UdpServer(service_t service, uint16_t port = glocalport)
:_sockfd(gsockfd)
,_localport(port)
,_isrunning(false)
,_service(service)
{}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024] = { 0 };
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
buffer[n] = '\0';
std::string result = _service(buffer);
sendto(_sockfd, result.c_str(), result.size(), 0, (const struct sockaddr*)&peer, len);
}
else
{
break;
}
}
_isrunning = false;
}
// ....
service_t _service;
};
而在单词处理的模块,将字典文件用unordered_map
的结构存放,遍历查找
const std::string sep = ": ";
class Dict
{
protected:
void LoadDict()
{
std::ifstream in(_dict_path.c_str());
if(!in.is_open())
{
LOG(FATAL, "open %s error", _dict_path.c_str());
exit(1);
}
std::string line;
while(getline(in, line))
{
if(line.empty()) continue;
size_t pos = line.find(sep);
if(pos == std::string::npos) continue;
std::string key = line.substr(0, pos);
if(key.empty()) continue;
std::string value = line.substr(pos+sep.size());
if(value.empty()) continue;
_dict[key] = value;
LOG(DEBUG, "load %s success", line.c_str());
}
LOG(DEBUG, "load dict done....");
}
public:
Dict(const std::string &path)
:_dict_path(path)
{
LoadDict();
}
std::string Translate(const std::string &word)
{
for(auto &[x, y] : _dict)
if(x == word) return y;
return "None";
}
~Dict()
{}
protected:
std::unordered_map<std::string, std::string> _dict;
std::string _dict_path;
};
在udp
服务器的调用中,将Dict::Translate
作为方法传给udp
服务器
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cout << "Usage:" << argv[0] << " port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
Dict dict("./dict.txt");
service_t translate = std::bind(&Dict::Translate, &dict, std::placeholders::_1);
std::unique_ptr<UdpServer> udp_server = std::make_unique<UdpServer>(translate, port);
udp_server->Initialize();
udp_server->Start();
return 0;
}
[Practice/Lesson1/2. dict_server · baiyahua/Linux - 码云 - 开源中国 (gitee.com)](https://gitee.com/baiyahua/linux/tree/master/Practice/Lesson1/2. dict_server)
4. chat_server
在第一份代码的基础上,我们想实现一个简单的群聊系统,udp
服务器只负责IO,将收到的数据交给消息转发模块,消息转发模块根据在线用户列表转发给每一个在线的人(包括发送信息的人)
要想IO逻辑与业务逻辑解耦,就需要给udp
服务器传递消息转发的函数指针,当服务器拿到数据时,去回调方法
而我们的消息转发模块,就是根据在线用户列表依次转发消息
#include "InetAddr.hpp"
#include "Log.hpp"
#include <iostream>
#include <vector>
class Route
{
protected:
void CheckOnline(InetAddr& who)
{
for(auto &user : _online_user)
{
if(user == who)
{
return; // _ip和port都相等,这样一款软件能起多个client
LOG(DEBUG, "%s is online", who.AddrStr().c_str());
}
}
_online_user.push_back(who);
LOG(DEBUG, "%s is not online, add it...", who.AddrStr().c_str());
}
void Offline(InetAddr& who)
{
std::vector<InetAddr>::iterator it = _online_user.begin();
while(it != _online_user.end())
{
if((*it) == who)
{
_online_user.erase(it);
break;
}
}
}
void ForwardHelper(int sockfd, const std::string &message)
{
for(auto &user : _online_user)
{
struct sockaddr_in peer = user.Addr();
sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&peer, sizeof(peer));
}
}
public:
Route()
{}
void Forward(int sockfd, const std::string& message, InetAddr& who)
{
// 1. 检查 who 在不在 _online_user中,如果不在,添加到 _online_user
CheckOnline(who);
// 如果 "quit" || "q",将退出的信息也转发给其他所有人
if(message == "quit" || message == "q")
Offline(who);
ForwardHelper(sockfd, message);
}
~Route()
{}
protected:
std::vector<InetAddr> _online_user;
};
这里消息转发模块我们想使用多进程的方式,提高效率
将我们之前写过的多线程引入进来
void Forward(int sockfd, const std::string& message, InetAddr& who)
{
// 1. 检查 who 在不在 _online_user中,如果不在,添加到 _online_user
CheckOnline(who);
// 如果 "quit" || "q",将退出的信息也转发给其他所有人
if(message == "quit" || message == "q")
Offline(who);
//ForwardHelper(sockfd, message);
task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message);
ThreadPool<task_t>::GetInstance()->Equeue(t);
}
但此时仍有问题,我们发现服务端能同时读写,证明udp
是支持全双工通信的,但客户端如果不输入发送的信息,收到的数据就会堆积在OS内部,上部不能接受,因此,我们的客户端也要写成多线程
using namespace byh;
int Initialize()
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cout << "create socket error";
exit(1);
}
std::cout << "create socket success" << std::endl;
return sockfd;
}
void Receive(int sockfd, std::string name)
{
while(true)
{
char buffer[1024] = { 0 };
struct sockaddr_in temp;
socklen_t length = sizeof(temp);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &length);
if(n > 0)
{
buffer[n] = '\0';
std::cerr << buffer << std::endl;
}
else
{
break;
}
}
}
void Send(int sockfd, uint16_t port, const std::string &ip, std::string name)
{
struct sockaddr_in server;
socklen_t len = sizeof(server);
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
while(true)
{
std::string line;
std::cout << "please enter->";
getline(std::cin, line);
ssize_t n = sendto(sockfd, line.c_str(), line.size(), 0, (const struct sockaddr*)&server, len);
if(n <= 0)
{
break;
}
}
}
// client需要知道server的ip和port
// ./udpclient serverip sererport
int main(int argc, char* argv[])
{
if(argc != 3)
{
std::cout << "Usage:" << argv[0] << " serverip serverport" << std::endl;
exit(0);
}
int sockfd = Initialize();
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
Thread recvt("recvive-thread", std::bind(&Receive, sockfd, std::placeholders::_1));
Thread sendt("send-thread", std::bind(&Send, sockfd, serverport, serverip, std::placeholders::_1));
recvt.Start();
sendt.Start();
recvt.Join();
sendt.Join();
close(sockfd);
return 0;
}
- 当转发消息时,我们也希望知道是谁发送的消息
- 检查是否在线、下线、转发逻辑都需要遍历
_online_user
,需要加锁
我们将客户端收到的数据向标准错误打印,在启动客户端的使用重定向将标准错误重定向到管道文件或指定终端文件下,就能直接看到客户端收到的数据
[Practice/Lesson1/3. chat_server · baiyahua/Linux - 码云 - 开源中国 (gitee.com)](https://gitee.com/baiyahua/linux/tree/master/Practice/Lesson1/3. chat_server)
5. 地址转化函数
上面我们使用的inet_ntoa
函数是将结构化的IP地址转化成字符串类型,根据man
手册的描述,转化后的char*
类型的IP地址存放在静态区中
经过测试,发现如果第二次再使用inet_ntoa
进行地址转换,会将原来的空间覆盖
如果在多线程下,需要考虑线程线程安全
有更安全的转换函数
#include <arpa/inet.h>
# 字符换类型ip->结构化ip + 主机序列->网络序列
int inet_pton(int af, const char *src, void *dst);
# 将结构化ip->字符串类型ip
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);