一、UDP Socket编程接口
c
// 创建套接字
int socket(int domain, int type, int protocol);
// 参数:
// domain:域(协议家族),这里使用 AF_INET 表示进行网络编程
// type:网络通信传输的类型,这里选择 SOCK_DGRAM 表示是使用UDP协议的面向数据报传递信息
// protocol:这个参数目前置0即可
// 返回值:成功则返回一个文件描述符(所以创建套接字的本质就是创建了一个文件);失败则返回-1,并设置对应的错误码
// 填充网络信息并绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 参数:
// sockfd:套接字文件描述符
// addr:这是为了统一socket接口而定义的数据结构,其中的 sockaddr_in 是我们今天网络通信所要用到的,我们需要在绑定之前设置好它的协议家族,端口号和ip地址
// addrlen:这是将addr的大小传递给内核,方便判断是哪种addr
// 返回值:成功返回0;失败则返回-1,并设置对应的错误码
// 发消息
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
// 参数:
// sockfd:发送信息主机的套接字
// buf:发送的内容
// len:发送内容的长度
// flags:发送方式(一般置为0即可)
// dest_addr:接收消息的主机的addr(用于指定ip+端口号)
// addrlen::接收消息的主机的addr的长度
// 返回值:成功返回发送的字节数;失败则返回-1,并设置对应的错误码
// 收消息
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
// 参数:
// sockfd:接收消息的主机的套接字
// buf:用以接收消息的缓冲区
// len:用以接收消息的缓冲区的长度
// flags:接收方式(一般置0即可)
// src_addr:用以接收 发送消息的主机的addr(知道是哪个ip+端口号发送到信息)
// addrlen:发送消息的主机的addr长度
//返回值:成功则返回成功接收的消息的字节数;失败则返回-1,并设置对应的错误码
二、UDP Socket编程
2.1 Echo Sever
这里我们编写一个简单的程序,当客户端向我发送数据后,服务端会将信息重新发回给客户端。
这里我们将ip和port的转换封装成一个类:
cpp
//InetAddr.hpp
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr
{
// 网络IP转主机IP
void IPNet2Host()
{
// 这里的转化我们不使用inet_ntoa,因为他还是不太安全的
char buffer[1024];
::inet_ntop(AF_INET, &_in_addr.sin_addr.s_addr, buffer, sizeof(buffer) - 1);
_ip = buffer;
}
// 网络端口号转主机端口号
void PortNet2Host()
{
_port = ::ntohs(_in_addr.sin_port);
}
public:
// 构造函数
InetAddr() = default;
InetAddr(const sockaddr_in &addr) : _in_addr(addr)
{
IPNet2Host();
PortNet2Host();
}
InetAddr(std::string ip, uint16_t port) : _ip(ip), _port(port)
{
_in_addr.sin_family = AF_INET;
_in_addr.sin_port = ::htons(_port);
_in_addr.sin_addr.s_addr = ::inet_addr(ip.c_str());
}
InetAddr(uint16_t port) : _port(port)
{
// 云服务器的公网IP不允许被绑定,但是虚拟机可以
// 所以在绑定云服务器时只需要给定端口号即可,IP使用INADDR_ANY
//在网络编程中,当一个进程需要绑定一个网络端口以进行通信时,可以使用INADDR_ANY 作为 IP 地址参数。这样做意味着该端口可以接受来自任何 IP 地址的连接请求,无论是本地主机还是远程主机。例如,如果服务器有多个网卡(每个网卡上有不同的 IP 地址),使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个网卡/IP 地址上面获取的。
_in_addr.sin_family = AF_INET;
_in_addr.sin_port = ::htons(_port);
_in_addr.sin_addr.s_addr = INADDR_ANY;
}
std::string Ip() { return _ip; }
uint16_t Port() { return _port; }
struct sockaddr *NetAddr() { return (struct sockaddr *)&_in_addr; }
socklen_t Len() { return sizeof(_in_addr); }
~InetAddr() {}
private:
struct sockaddr_in _in_addr;
std::string _ip;
uint16_t _port;
};
接下来编写服务端代码:
cpp
//UDPSever.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogMudule;
// 默认端口号
const static uint16_t defaultport = 8080;
// const static std::string defaultip = "127.0.0.1";//本地通信测试阶段用到的,网络通信不需要
class UDPSever
{
public:
UDPSever(uint16_t port = defaultport) : _in_addr(port), _isrunning(false)
{
// 1. 创建套接字
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(1);
}
LOG(LogLevel::DEBUG) << "socket succeed";
// 2. 绑定
int n = ::bind(_sockfd, _in_addr.NetAddr(), _in_addr.Len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::DEBUG) << "bind succeed";
}
void Run()
{
_isrunning = true;
char buff[1024];
// 定义接收发送消息的主机的addr信息的结构
struct sockaddr_in src_addr;
socklen_t len;
while (true)
{
// 接收来自客户端的信息
int n = ::recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&src_addr, &len);
InetAddr cli(src_addr);
if (n > 0)
{
buff[n] = '\0';
LOG(LogLevel::INFO) << cli.Ip() << ":" << cli.Port() << "Clint say#" << buff;
std::string message = "Echo#";
message += buff;
// 将包装的信息发送给源主机
::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&src_addr, len);
}
}
_isrunning = false;
}
~UDPSever()
{
close(_sockfd);
}
private:
int _sockfd;
InetAddr _in_addr; // 用InetAddr 来管理转化操作
bool _isrunning;
};
cpp
// UDPSever.cc
#include <memory>
#include "UDPSever.hpp"
int main()
{
std::unique_ptr<UDPSever> us_ptr = std::make_unique<UDPSever>();
us_ptr->Run();
return 0;
}
最后写客户端代码:
cpp
//UDPClient.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InetAddr.hpp"
class UDPClient
{
public:
UDPClient(uint16_t port, std::string ip) : dst_addr({ip, port})
{
// 1.创建套接字
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(1);
}
// 2.绑定?
// 不需要
}
void Run()
{
std::string message;
while (true)
{
std::cout << "Please Enter # ";
std::getline(std::cin, message);
// 向目标主机发送message
::sendto(_sockfd, message.c_str(), message.size(), 0, dst_addr.NetAddr(), dst_addr.Len());
// 用以接收由Sever发送回来的数据
char buff[1024];
sockaddr_in src_addr;
socklen_t len;
int n = ::recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&src_addr, &len);
if (n > 0)
{
buff[n] = '\0';
std::cout << buff << std::endl;
}
}
}
~UDPClient()
{
close(_sockfd);
}
private:
int _sockfd;
InetAddr dst_addr; // 目标主机结构
};
cpp
//UDPClient.cc
#include <memory>
#include "UDPClient.hpp"
//./UDPClient 127.0.0.1 8888
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage Error" << std::endl;
exit(-1);
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
std::unique_ptr<UDPClient> uc_ptr = std::make_unique<UDPClient>(port, ip);
uc_ptr->Run();
return 0;
}
这里怎么能够验证我们的程序编写是成功的呢?这里我们就来介绍几条命令:
ping : 用于检查本地主机到目标主机的网络是否可达,测量网络延迟,评估网络质量。
bashcaryon@VM-24-10-ubuntu:~/linux/TCP_Socker/EchoServer$ ping -c5 62.234.18.77 PING 62.234.18.77 (62.234.18.77) 56(84) bytes of data. 64 bytes from 62.234.18.77: icmp_seq=1 ttl=63 time=0.913 ms 64 bytes from 62.234.18.77: icmp_seq=2 ttl=63 time=0.993 ms 64 bytes from 62.234.18.77: icmp_seq=3 ttl=63 time=0.957 ms 64 bytes from 62.234.18.77: icmp_seq=4 ttl=63 time=0.951 ms 64 bytes from 62.234.18.77: icmp_seq=5 ttl=63 time=0.972 ms --- 62.234.18.77 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 4009ms rtt min/avg/max/mdev = 0.913/0.957/0.993/0.026 ms
netstat: 用于查看网络状态
常用选项:
• n 拒绝显示别名,能显示数字的全部转化成数字
• l 仅列出有在 Listen (监听) 的服务状态
• p 显示建立相关链接的程序名
• t (tcp)仅显示 tcp 相关选项
• u (udp)仅显示 udp 相关选项
• a (all)显示所有选项,默认不显示 LISTEN 相关
bashcaryon@VM-24-10-ubuntu:~$ netstat -uap (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name udp 0 0 _localdnsproxy:domain 0.0.0.0:* - udp 0 0 _localdnsstub:domain 0.0.0.0:* - udp 0 0 VM-24-10-ubuntu:bootpc 0.0.0.0:* - udp 0 0 0.0.0.0:51398 0.0.0.0:* 486728/./client_udp udp 0 0 localhost:323 0.0.0.0:* - udp 0 0 0.0.0.0:8080 0.0.0.0:* 486145/./sever_udp udp6 0 0 ip6-localhost:323 [::]:* -
pidof: 查看服务器的进程 id
bashcaryon@VM-24-10-ubuntu:~$ pidof sever_udp 486145
我们的客户端代码编写时留下了一个问题,为什么客户端不需要绑定端口号呢?
首先我们要明确,客户端也必须要有自己的ip和端口号,但是客户端是不需要自己显示调用bind的,因为一个端口号只能被绑定一次,客户端的端口号实在sendto时由操作系统自行绑定的。
2.2 Dictionary Sever
上面的Echo Sever没有让我们感受到网络服务的用处,这里我们使用一个Dictionary Sever来进行让网络通信完成一个小任务。
首先让我们看一下我们的小字典相关的文件:
bash
# dictionary.txt
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
cpp
// Dictionary.hpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
const static std::string defaultfilepath="./";
const static std::string defaultfilename="dictionary.txt";
const static std::string defaultsep=": ";
class Dictionary
{
//切割字符串
bool SplitString(const std::string& line,std::string* key,std::string* value,const std::string& sep=defaultsep)
{
auto pos=line.find(sep);
if(pos==line.npos) return false;
*key=line.substr(0,pos);
*value=line.substr(pos+sep.size());
if(key->empty()||value->empty()) return false;
return true;
}
//加载文件
void Load()
{
//C++17文件操作
std::string file=_filepath+_filename;
std::ifstream in(file.c_str());
if(!in.is_open())
{
std::cerr<<"file open failed"<<std::endl;
exit(-1);
}
std::string line;
while(std::getline(in,line))
{
//apple: 苹果
std::string key;
std::string value;
if (SplitString(line, &key, &value))
{
_dictionary.insert(std::make_pair(key, value));
}
}
in.close();
}
public:
Dictionary(const std::string& filepath=defaultfilepath,const std::string& filename=defaultfilename)
:_filepath(filepath),_filename(filename)
{
Load();
}
//翻译
std::string translate(const std::string& str)
{
auto posit=_dictionary.find(str);
if(posit==_dictionary.end()) return "None";
else return posit->second;
}
~Dictionary()
{
}
private:
std::unordered_map<std::string,std::string> _dictionary;
const std::string _filepath;
const std::string _filename;
};
接下来我们改造一下我们的服务端:
cpp
// TCPSever.hpp
//......这一部分没有发生变化
// 功能模块
using func_t = std::function<std::string(const std::string &)>;
class UDPSever
{
public:
//构造函数时需传递功能func,其他部分没有发生变化
UDPSever(func_t func, uint16_t port = defaultport) : _func(func), _in_addr(port), _isrunning(false)
{
// 1. 创建套接字
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(1);
}
LOG(LogLevel::DEBUG) << "socket succeed";
// 2. 绑定
int n = ::bind(_sockfd, _in_addr.NetAddr(), _in_addr.Len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::DEBUG) << "bind succeed";
}
//这一部分让我们的函数执行func回调方法,并将结果返回给客户端
void Run()
{
_isrunning = true;
char buff[1024];
// 定义接收发送消息的主机的addr信息的结构
struct sockaddr_in src_addr;
socklen_t len;
while (true)
{
// 接收来自客户端的信息
int n = ::recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&src_addr, &len);
InetAddr cli(src_addr);
if (n > 0)
{
buff[n]=0;
// 将传来了词进行翻译
string result=_func(buff);
// 将翻译的结果发送给源主机
InetAddr peer(src_addr);
int n=::sendto(_sockfd, result.c_str(), result.size(), 0, peer.NetAddr(), peer.Len());
}
}
_isrunning = false;
}
//......未发生变化
private:
int _sockfd;
InetAddr _in_addr; // 用InetAddr 来管理转化操作
bool _isrunning;
func_t _func; //新增功能模块
};
对应的也就需要我们的服务端需要将翻译模块传过去,我们这样的最大好处是减少了代码的耦合度,使得字典文件和服务端文件成功分离。
cpp
// UDPSever.cc
#include <memory>
#include "UDPSever.hpp"
#include "Dictionary.hpp"
int main()
{
//传入功能
Dictionary d;
std::unique_ptr<UDPSever> us_ptr = std::make_unique<UDPSever>([&d](const std::string& string)->std::string{
return d.translate(string);
});
us_ptr->Run();
return 0;
}
至于客户端等文件并未修改。
2.3 Chat Sever
我们的目标是要设计出来一个mini的群聊,下图是我们的设计demo
接下来就是我们的设计。
首先我们需要对参与聊天的用户进行管理,所以我们基于观察者模式设计出了用户管理类。
cpp
//User.hpp
#pragma once
#include <algorithm>
#include <list>
#include <string>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "mutex.hpp"
using namespace LogMudule;
// 为了代码的可扩展性,让用户类来继承用户接口
class UserInterface
{
public:
virtual ~UserInterface() = default;
virtual void SendTo(int sockfd, std::string message) = 0;
virtual bool operator==(const InetAddr &user) = 0;
};
//用户类
class User : public UserInterface
{
public:
User(const InetAddr &user) : _user(user)
{
}
void SendTo(int sockfd, std::string message) override
{
LOG(LogLevel::INFO) << "send message to " << _user.Addr() << "Info is :" << message;
::sendto(sockfd, message.c_str(), message.size(), 0, _user.NetAddr(), _user.Len());
}
bool operator==(const InetAddr &user) override
{
return _user == user;
}
~User() override
{
}
private:
InetAddr _user;
};
// 观察者模式编写用户管理
class UserManage
{
public:
UserManage() {}
// 添加在线用户
void AddUser(InetAddr &user)
{
// 多线程访问需要加锁
_mutex.Lock();
for (auto &u : _online_user)
{
if (*u == user)
{
LOG(LogLevel::INFO) << user.Addr() << " 用户已存在";
//不释放会导致死锁
_mutex.Unlock();
return;
}
}
_online_user.push_back(std::make_shared<User>(user));
LOG(LogLevel::INFO) << "添加新用户"<<user.Addr();
_mutex.Unlock();
}
// 轮询发送信息
void Router(int sockfd, std::string message)
{
_mutex.Lock();
for (auto &u : _online_user)
{
u->SendTo(sockfd, message);
}
_mutex.Unlock();
}
// 删除用户
void DeleteUser(InetAddr &user)
{
// 不会删除只会将目标元素移动到末尾
auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&user](std::shared_ptr<UserInterface>& u) -> bool
{ return *u == user; });
_online_user.erase(pos,_online_user.end());
}
~UserManage() {}
private:
std::list<std::shared_ptr<UserInterface>> _online_user;
MutexModel::Mutex _mutex;
};
再接下来我们要将用户类和服务端耦合,我们要让线程池来替我们解决客户端发送的任务。
cpp
//sever_udp.hpp
//......
#include "ThreadPool.hpp"
using namespace ThreadPoolModual;
//......
// 线程池的任务
using task_t = std::function<void()>;
// 添加用户
using adduser_t = std::function<void(InetAddr &user)>;
// 轮询任务
using route_t = std::function<void(int sockfd, std::string message)>;
// 删除用户
using deletuser_t = std::function<void(InetAddr &user)>;
class UDPSever
{
public:
// 需要注入的任务序列
void Regester(adduser_t add_user, route_t route, deletuser_t deletuser)
{
_add_user = add_user;
_route = route;
_delet_user = deletuser;
}
void Run()
{
_isrunning = true;
char buff[1024];
// 定义接收发送消息的主机的addr信息的结构
struct sockaddr_in src_addr;
socklen_t len = sizeof(src_addr);
while (true)
{
// 接收来自客户端的信息
int n = ::recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&src_addr, &len);
if (n > 0)
{
InetAddr cli(src_addr);
buff[n] = '\0';
std::string message;
if (strcmp(buff, "quit") == 0)
{
_delet_user(cli);
message=cli.Addr()+" 我走了,你们聊";
}
else
{
// 添加用户
_add_user(cli);
// 合成新的信息
message = cli.Addr() + "say #" + buff;
}
// 需要执行的任务就是User.hpp中的Router
task_t task = std::bind(_route, _sockfd, message);
// 让线程池进程调度轮转
ThreadPool<task_t>::GetInstance()->Enqueue(task);
}
}
_isrunning = false;
}
~UDPSever()
{
close(_sockfd);
}
private:
//......
adduser_t _add_user;
route_t _route;
deletuser_t _delet_user;
};
接下来在我们运行起来后会发现客户端只有输入一条消息才能看到其他人发送的消息,这是因为单线程的缘故,所以我们要将客户端的收发信息独立开来
cpp
//client.hpp
//......
#include "signal.h"
void *Receive(void *args)
{
int sockfd=*(int*)args;
while(true)
{
// 用以接收由Sever发送回来的数据
char buff[1024];
sockaddr_in src_addr;
socklen_t len;
int n = ::recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&src_addr, &len);
if (n > 0)
{
buff[n] = '\0';
std::cerr << buff << std::endl;
}
}
return nullptr;
}
class UDPClient
{
public:
//......
void Run()
{
pthread_t pid;
// 多线程执行任务,主线程发消息,新线程收消息
pthread_create(&pid, nullptr, Receive, (void*)&_sockfd);
std::string message;
//const std::string online ="我来了呀!";
//::sendto(_sockfd, online.c_str(), online.size(), 0, dst_addr.NetAddr(), dst_addr.Len());
while (true)
{
std::cout << "Please Enter # ";
std::getline(std::cin, message);
// 向目标主机发送message
::sendto(_sockfd, message.c_str(), message.size(), 0, dst_addr.NetAddr(), dst_addr.Len());
}
}
void Quit(int signo)
{
::sendto(_sockfd, "quit", sizeof("quit"), 0, dst_addr.NetAddr(), dst_addr.Len());
exit(0);
}
//......
private:
int _sockfd;
InetAddr dst_addr; // 目标主机结构
};
对应的也要更新我们的客户端和服务端的主函数:
cpp
//sever_udp.cc
#include <memory>
#include "UDPSever.hpp"
#include "User.hpp"
int main()
{
//用户管理模块
std::shared_ptr<UserManage> um =std::make_shared<UserManage>();
//网络服务模块
std::unique_ptr<UDPSever> us_ptr = std::make_unique<UDPSever>();
us_ptr->Regester([&um](InetAddr &user){ um->AddUser(user); },
[&um](int sockfd, const std::string &message){ um->Router(sockfd, message);},
[&um](InetAddr &user){ um->DeleteUser(user);}
);
us_ptr->Run();
return 0;
}
cpp
//client_udp.cc
#include <memory>
#include <functional>
#include "UDPClient.hpp"
std::unique_ptr<UDPClient> uc_ptr ;
//./UDPClient 127.0.0.1 8888
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage Error" << std::endl;
exit(-1);
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
uc_ptr = std::make_unique<UDPClient>(port, ip);
signal(2,[](int signo){
uc_ptr->Quit(signo);
});
uc_ptr->Run();
return 0;
}
三、Windows 下的UDP Socket
接下来我们来看一下Windows下的UDP Socket接口及其代码,来进行一下跨平台通信的测试
cpp
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = "62.234.18.77"; // 填写你的云服务器 ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号
int main()
{
WSADATA wsd;
WSAStartup(MAKEWORD(2, 2), &wsd);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport); //?
server.sin_addr.s_addr = inet_addr(serverip.c_str());
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == SOCKET_ERROR)
{
std::cout << "socker error" << std::endl;
return 1;
}
std::string message;
char buffer[1024];
while (true)
{
std::cout << "Please Enter@ ";
std::getline(std::cin, message);
if (message.empty()) continue;
sendto(sockfd, message.c_str(), (int)message.size(), 0,(struct sockaddr*)&server, sizeof(server));
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
closesocket(sockfd);
WSACleanup();
return 0;
}
最后附上windows linux 通信结果: