前面的文章中我们叙述了网络编程套接字的一些预备知识点,从本文开始我们就将开始UDP套接字的编写。本文中的服务端与客户端都是在阿里云的云服务器进行编写与测试的。
在v1的版本中我们先来使用一下前面讲过得一些接口,简单的构建一个udp服务器:
// udp_server.cc
#include "udp_server.hpp"
#include
using namespace std;
using namespace ns_server;
int main()
{
unique_ptr<UdpServer> usvr(new UdpServer("1.1.1.1", 8082)); // 通过智能指针控制服务器的资源管理,并且向程序传入ip与端口号
usvr->InitServer(); //服务器的初始化
usvr->Start();
return 0;
}
// udp_server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace ns_server
{
// 用于展示返回对应的错误提示
enum{
SOCKET_ERR = 1,
BIND_ERR
};
const static uint16_t default_port = 8080;
class UdpServer
{
public:
UdpServer(std::string ip, uint16_t port = default_port) : ip_(ip), port_(port)
{
std::cout << "server addr: " << ip << " : " << port_ << std::endl;
}
void InitServer()
{
// 1. 创建socket接口,打开网络文件
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ < 0)
{
std::cerr << "create socket error: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << sock_ << std::endl; // 3
// 2. 给服务器指明IP地址和端口号Port
struct sockaddr_in local; // 这个 local 在哪里定义呢?用户空间的特定函数的栈帧上,不在内核中!
bzero(&local, sizeof(local)); // 清空上述字段ud
local.sin_family = AF_INET; // PF_INET 初始化socketaddr_in结构
local.sin_port = htons(port_); // 本地主机序列构建的port_,需要从主机序列转变成网络序列
// inet_addr: 1,2
// 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
// 2. 需要将主机序列转化成为网络序列
local.sin_addr.s_addr = inet_addr(ip_.c_str()); // sin_addr C++中的结构体在C++中可以进行转化,但是在C语言中不行
// 这里需要将字符串转换uint32_t的类型,并且同时进行将主机序列转换成网络序列
// inet_addr 函数将包含 IPv4 点十进制地址的字符串转换为IN_ADDR结构的正确地址。而在in_addr结构之中有in_addr_t s_addr的一个数据结构
if (bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0) // 然后是绑定相关的套接字文件,此时需要就将前面在帧栈上定义的local进行绑定
{
std::cerr << "bind socket error: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << sock_ << std::endl; //3
}
void Start() {}
~UdpServer() {}
private:
int sock_;
uint16_t port_;
std::string ip_; // 后面需要去掉这个ip
};
}
然后运行上述的程序会出现一个问题就是:
server addr: 1.1.1.1 : 8082
create socket success: 3
bind socket error: Cannot assign requested address
bind socket error: Cannot assign requested address 云服务器不需要bind ip地址,需要让服务器自己制定ip地址
云服务器,或者一款服务器,一般不要指明某一个确定的IP – 服务器可能有多张网卡,可能配有多个IP,我们要让我们的udpserver启动的时候bind本主机上的任意IP,然后我们对上述的v1版本进行修改。
// 需要修改的地方就是:
local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,bind本主机上的任意IP
server addr: 1.1.1.1 : 8082
create socket success: 3
bind socket success: 3
此时就可以正确的进行bind操作。
下面我们将上述的程序进行完善,添加上服务器正常工作的程序(我们想要完成的是客户端发送消息,服务端接收到消息并答应在终端上,同时将消息返回给客户端)
void Start()
{
// 服务器的正常工作
char buffer[1024];
while (true)
{
// 收
// ssize_t 实际写入的大小 recvfrom(int sockfd 绑定的套接字, void *buf 接受数据存放的缓冲区, size_t len 缓冲区长度, int flags 读取方式(0), struct sockaddr *src_addr 需要知道client的IP和PORT 输入接收缓冲区, socklen_t *addrlen 实际结构体的大小); 输入输出型参数 需要知道谁发的数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 这里一定要写清楚,未来传入的缓冲区的大小
int n = recvfrom(sock_, buffer, sizeof(buffer)-1/*因为这是以字符作为消息的类型,所以缓冲区预留一部分空间*/, 0, (struct sockaddr*)&peer, &len); // 消息的类型需要程序员来定义
if (n > 0) buffer[n] = '\0';
else continue;
// 提取client信息
std::string clientip = inet_ntoa(peer.sin_addr); // 把一个四字节的IP转化为字符串
uint16_t clientport = ntohs(peer.sin_port); // 将从网络中获取的端口号转换成主机
std::cout << clientip << "-" << clientport << "#" << buffer << std::endl;
// 发
sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len); // 往文件中去写的时候,不需要携带\0
}
}
然后对udp_server.cc文件进行修改,我们想要使用./udp_server port
的形式来运行程序,在运行程序的时候将端口号进行传入
static void usage(string proc)
{
std::cout << "Usage:\n\t" << proc << " port\n" << std::endl;
}
// ./udp_server port
int main(int argc, char* argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]); // 此处为char*类型的数据若是要传入port需要进行转换
unique_ptr<UdpServer> usvr(new UdpServer(port));
// bind socket error: Cannot assign requested address 云服务器不需要bind ip地址,需要让服务器自己制定ip地址
// 自己本地装的虚拟机或者是物理机器是允许的,
usvr->InitServer(); //服务器的初始化
usvr->Start();
return 0;
}
然后是服务端的代码
// udp_client.cc
#include
#include
// 127.0.0.1 本地回环,表示的就是当前的主机,通常用来进行本地通信或者测试
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKET_ERR);
}
// client 这里要不要bind? 要的!socket通信的本质[clientip:clientport, serverclient. serverport]
// 要不要自己bind?不需要自己bind,os自动给我们进行bind!-- 为什么?client的port要随机让os分配防止client出现启动冲突
// server为什么要自己bind -- 1. server的端口不能随意改变, 众所周知且不能随意改变;2. 同一家公司的port需要统一进行管理
// 明确server是谁?
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());
while (true)
{
// 用户输入
std::string message;
std::cout << "please Enter# ";
std::cin >> message;
// 什么时候bind?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport + 自己的IP 1. bind 2. 构建发送的数据报文
// 发送
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
// 接收
char buffer[2048];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
下面我们还需要对上述的UDP网络通信进行修改,在上述的网络通信程序中服务端在接收到客户端的信息之后立即进行了处理,然后将信息进行返回,但是我们想要让信息的处理与网络IO进行分离,在第三版中我们就进行对应的修改。我们在工作空间中定义了using func_t = std::function
这样的参数为string,返回值为string的一个包装器,在udp_server类中新增了一个成员func_t service_;
来进行业务处理。在编写构造函数的时候将这个成员初始化,那么我们就可以在外部传入一个执行业务的方法,在外部定义好了这个执行的方法,在外部即udp_server.cc中将业务处理完毕之后再进行网络IO。
下面就是对上述版本的修改
// udp_server.hpp
// 在Start()中发送消息前执行业务处理
void Start()
{
// ...
// 做业务处理
std::string response = service_(buffer);
// 发
sendto(sock_, response.c_str(), response.size(), 0, (struct sockaddr*)&peer, len); // 往文件中去写的时候,不需要携带\0
}
然后我们就在cpp文件中处理上层业务
// udp_server.cc
std::string transactionString(std::string request)
{
std::string result;
char c;
for(auto & r : request)
{
if (islower(r))
{
c = toupper(r);
result.push_back(c);
}
else
{
result.push_back(r);
}
}
return result;
}
static bool isPass(const std::string& command)
{
bool pass = true;
auto pos = command.find("mv");
if (pos != std::string::npos) pass = false;
pos = command.find("rm");
if (pos != std::string::npos) pass = false;
return pass;
}
std::string execteCommand(std::string command)
{
// FILE *popen(const char *command, const char *type);
// 1. 创建管道
// 2. 创建子进程
// 3. 通过FILE*将结果直接返回,可以让用户以读取文件的访问,获得命令执行的结果
// 安全检查
if (!isPass(command)) return "bad";
// 业务逻辑处理
FILE* fp = popen(command.c_str(), "r");
if (fp == nullptr) return "None";
// 获取结果
char line[2048];
std::string result;
while (fgets(line, sizeof(line), fp) != NULL)
{
result += line;
}
return result;
fclose(fp);
}
int main()
{
// ...
// 通过传入第一个transactionString函数可以将我们客户端输入的小写字符,在服务端转换成大写然后在返回客户端
unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port));
// 这个业务方法就是将客户端输入的指令发送到服务端,在服务端执行后再将执行的结果返回给客户端
// unique_ptr usvr(new UdpServer(execteCommand, port));
}
上述第三版的程序还有一些问题就是,程序运行的时候收发的处理都在start()函数中,但是这样一旦阻塞在发送或者阻塞在收取的时候,假如有多个客户端要连接服务端,就会有影响。那么在第四版中我们就将结合前面的讲述过的基于环形队列的生产消费模型来将接受与发送分别使用两个线程来处理。在第四版中我们想要实现的是一个简易的udp多人聊天程序。
// udp_server.hpp
class UdpServer
{
public:
UdpServer(uint16_t port = default_port) :port_(port)
{
pthread_mutex_init(&lock, nullptr);
p = new Thread(1, std::bind(&UdpServer::Recv, this)); // 这里传入接受与发送的函数时如果直接传入会发生报错,以为Recv与Broadcast都是类的方法,而类的方法是有隐含的this指针的,此时就可以使用bind函数将this指针这个成员先绑定,然后就可以正常的运行。
c = new Thread(2, std::bind(&UdpServer::Broadcast, this));
}
void Start()
{
// ...
c->run();
p->run();
}
void addUser(const std::string& name, const struct sockaddr_in& peer) // 构建一个新用户
{
LockGuard lockguard(&lock);
// onlineuser[name] = peer;
auto iter = onlineuser.find(name); // 遍历检测是否存在该用户
if (iter != onlineuser.end()) return;
onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));
}
void Recv()
{
// 服务器的正常工作
char buffer[1024];
while (true)
{
// 收
// ssize_t 实际写入的大小 recvfrom(int sockfd 绑定的套接字, void *buf 接受数据存放的缓冲区, size_t len 缓冲区长度, int flags 读取方式(0), struct sockaddr *src_addr 需
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 这里一定要写清楚,未来传入的缓冲区的大小
int n = recvfrom(sock_, buffer, sizeof(buffer)-1/*因为这是以字符作为消息的类型,所以缓冲区预留一部分空间*/, 0, (struct sockaddr*)&peer, &len); // 消息的类型需要程序员
if (n > 0) buffer[n] = '\0';
else continue;
std::cout << "recv done ..." << std::endl;
// 提取client信息
std::string clientip = inet_ntoa(peer.sin_addr); // 把一个四字节的IP转化为字符串
uint16_t clientport = ntohs(peer.sin_port); // 将从网络中获取的端口号转换成主机
std::cout << clientip << "-" << clientport << "#" << buffer << std::endl;
// 构建一个用户并检查
std::string name = clientip;
name += "-";
name += std::to_string(clientport);
// 如果不存在就插入,如果存在,什么都不做
addUser(name, peer);
std::string message = name + " >> " + buffer;
rq.push(message); // 将接收到的信息存放入生产消费中
}
}
void Broadcast()
{
while (true)
{
std::string sendstring;
rq.pop(&sendstring); // 从生产消费模型中获取信息
std::vector<struct sockaddr_in> v; // 获取所有的用户使用数组进行记录
{
LockGuard lockguard(&lock); // 使用锁进行保护防止产生冲突
for (auto user : onlineuser)
{
v.push_back(user.second);
}
}
for (auto user : v) // 依次将信息发送出去
{
sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&user, sizeof(user));
}
}
}
~UdpServer()
{
pthread_mutex_destroy(&lock);
c->join();p->join();
delete p;delete c;
}
private:
// ...
std::unordered_map<std::string, struct sockaddr_in> onlineuser; // 添加使用用户
RingQueue<std::string> rq; // 基于环形队列的生产消费模型
pthread_mutex_t lock; // 互斥锁
Thread *c, *p; // 创建生产者与消费者线程
};
对于客户端同样可以使用多线程
udp_client.cc
void* recver(void* args)
{
pthread_detach(pthread_self());
int sock = *static_cast<int*>(args);
while (true)
{
// 接收
char buffer[2048];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&temp, &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
}
int main(int argc, char *argv[])
{
// ...
pthread_t tid;
pthread_create(&tid, nullptr, recver, &sock);
while (true)
{
// 用户输入
std::string message;
std::cerr << "please Enter# "; // 往2号文件发送
// std::cin >> message;
getline(std::cin, message);
// 发送
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
return 0;
}