目录
UDP的概念
UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输层协议,正式规范为RFC 768。
-
无连接性:UDP是无连接的,这意味着在发送数据之前不需要建立连接。这一特点使得UDP能够减少开销和发送数据之前的时延,从而提高了传输效率。
-
不可靠性:UDP不提供可靠的数据传输服务。它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份。换句话说,UDP使用尽最大努力交付,但不保证数据能够可靠到达目的地。
-
面向报文:UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序需要负责处理可能出现的数据包乱序或丢失的情况。
-
支持多种交互通信:UDP支持一对一、一对多、多对一和多对多的交互通信方式,这使得它在某些特定场景下非常灵活和有用。
-
首部开销小:UDP的首部只有8个字节,相比TCP的20个字节的首部要短,这进一步减少了传输开销。
-
适用场景:由于UDP的上述特点,它特别适用于需要快速传输少量数据的场景,如实时应用程序(视频会议、在线游戏、语音聊天等),以及对数据传输速度要求较高、对数据完整性要求较低的场景。此外,UDP还常用于一些控制信息的传输,如路由协议、DNS协议等。
UDP以其高效、灵活和轻量级的特性,在特定场景下发挥着重要作用。然而,由于其不可靠性,使用UDP时需要谨慎处理数据丢失和乱序的问题。
简单的UDP回声服务器
服务端
这个UdpServer
类是一个简单的UDP回声服务器,能够接收客户端发送的数据,并将其原样返回。
类成员变量
-
_prot
: 存储UDP服务器要监听的端口号,默认为DefaultPort
(8888)。 -
_sockfd
: 存储套接字文件描述符,用于网络通信。默认值为DefaultSockfd
(-1),表示尚未创建套接字。
构造函数
-
UdpServer(uint16_t port = DefaultPort)
: 构造函数允许用户指定一个端口号,如果不提供,则使用默认端口DefaultPort
。
成员函数
-
Init()
:-
创建一个UDP套接字。
-
如果套接字创建失败,记录错误信息并退出程序。
-
绑定套接字到指定的IP地址(在这里是
INADDR_ANY
,表示接受任何IP地址的连接)和端口号。 -
如果绑定失败,记录错误信息并退出程序。
-
-
Start()
:-
这是一个无限循环,服务器将持续运行,等待并处理客户端的请求。
-
使用
recvfrom
函数从套接字接收数据,并保存到buffer
中。 -
如果接收到数据(
n > 0
),服务器会将接收到的数据打印到标准输出,并将相同的数据发送回客户端(即实现了一个简单的回声服务器)。
-
-
析构函数 (
~UdpServer()
):-
目前析构函数是空的,但通常在这里应该关闭套接字和释放相关资源。在这个简单的例子中,由于程序在出现错误时会直接退出,所以没有在析构函数中处理资源释放。
-
#pragma once
#include "ErrInfo.hpp"
#include "Log.hpp"
#include "NoCopy.hpp"
#include <arpa/inet.h>
#include <cstring>
#include <errno.h>
#include <iostream>
#include <netinet/in.h>
#include <queue>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <vector>
const static std::string DefaultIp = "0.0.0.0";
const static int DefaultSockfd = -1;
const static uint16_t DefaultPort = 8888;
const static int DefaultSize = 1024;
class UdpServer : public NoCopy
{
public:
UdpServer(uint16_t prot = DefaultPort)
: _prot(prot),
_sockfd(DefaultSockfd)
{
}
void Init()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
lg.LogMessage(Fatal, "Creat socket faile! errno:%d:%s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "Creat socket success! socket: %d\n", _sockfd);
// 绑定网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_prot);
local.sin_addr.s_addr = INADDR_ANY;
// 将结构体绑定到内核
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n != 0)
{
lg.LogMessage(Fatal, "bine err,%d ... %s\n", errno, strerror(errno));
exit(Bind_Err);
}
}
void Start()
{
char buffer[DefaultSize];
while (true)
{
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 say# " << buffer << std::endl;
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
uint16_t _prot;
int _sockfd;
};
服务端的启动
函数原型
int main(int argc, char *argv[])
-
argc
: 命令行参数的数量。 -
argv
: 命令行参数的数组,其中argv[0]
通常是程序的名称。
函数流程
-
检查命令行参数数量:
-
如果命令行参数数量不等于2(程序名称和一个端口号),则打印使用方法并返回错误代码
Usage_Err
。
-
-
解析端口号:
-
使用
std::stoi(argv[1])
将命令行中的第二个参数(即argv[1]
)从字符串转换为整数,这个整数表示UDP服务器要监听的端口号。
-
-
创建
UdpServer
对象:-
使用C++11中的
std::unique_ptr
智能指针来管理UdpServer
对象的生命周期。这样做的好处是,当unique_ptr
超出作用域或被销毁时,它会自动释放所指向的对象,从而防止内存泄漏。 -
std::make_unique<UdpServer>(port)
用于创建一个UdpServer
对象的实例,并将其端口号设置为从命令行参数解析得到的值。
-
-
初始化服务器:
-
调用
usvr->Init()
来初始化UDP服务器,包括创建套接字和绑定到指定的端口。
-
-
启动服务器:
-
调用
usvr->Start()
来启动UDP服务器。这个函数会进入一个无限循环,等待并处理来自客户端的请求。
-
-
程序结束:
-
函数返回0,表示程序正常结束。但实际上,由于
usvr->Start()
是一个无限循环,所以程序通常不会到达这一步,除非在Start
函数内部发生了某种错误或异常导致循环退出。
-
#include "UdpServer.hpp"
#include "ErrInfo.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
// ./udp_server 127.0.0.1 8888
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}
客户端
这是一个C++程序,它实现了一个简单的UDP客户端。以下是对这个程序的详细介绍:
包含的头文件
程序首先包含了一系列头文件,这些头文件提供了创建套接字、处理网络数据和输入/输出等功能的必要接口。
函数原型
-
void Usage(const std::string &process)
: 打印如何使用这个程序的说明。 -
int main(int argc, char *argv[])
: 程序的入口点。
main函数流程
-
检查命令行参数数量:
-
如果参数数量不等于3(程序名称、服务器IP和服务器端口),则打印使用方法并返回错误代码1。
-
-
解析服务器IP和端口:
-
从命令行参数中获取服务器IP和端口,端口被转换为整数。
-
-
创建套接字:
-
使用
socket
函数创建一个UDP套接字。如果创建失败,则打印错误信息并返回错误代码2。
-
-
初始化服务器地址结构:
-
使用
sockaddr_in
结构来表示服务器的地址。这个结构被初始化为0,然后设置其家族为AF_INET
(IPv4),端口号转换为网络字节序,并使用inet_addr
函数将服务器IP地址转换为网络字节序的整数形式。
-
-
主循环:
- 程序进入一个无限循环,等待用户输入要发送的数据。
-
提示用户输入数据,并使用
getline
函数从标准输入读取数据。 -
使用
sendto
函数将数据发送到服务器。如果发送成功,程序将继续执行;否则,跳出循环。 -
如果数据成功发送,程序将尝试使用
recvfrom
函数从服务器接收响应。如果接收到数据,程序将打印服务器的响应;否则,跳出循环。
-
- 程序进入一个无限循环,等待用户输入要发送的数据。
-
关闭套接字:
-
在退出循环之前,使用
close
函数关闭套接字。
-
-
程序结束:
-
返回0,表示程序正常结束。
-
#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "create socket success: " << sock << std::endl;
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 inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
// 我们要发给谁呀?server
ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));
if(n > 0)
{
char buffer[1024];
//收消息
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len); // 一般建议都是要填的.
if(m > 0)
{
buffer[m] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else
break;
}
else
break;
}
close(sock);
return 0;
}
可以传处理方法的简单udp服务器
服务端
这个UdpServer
类提供了一个简单的UDP服务器框架,用户可以通过提供自定义的消息处理函数来扩展其功能。
类定义
UdpServer
类继承自NoCopy
类,这意味着UdpServer
对象不能被复制,这通常是为了防止资源管理的复杂性或潜在的bug。
成员变量
-
_port
: 服务器的端口号,默认为8888。 -
_socket
: 套接字文件描述符,默认为-1,表示尚未创建。 -
_OnMessage
: 一个函数对象,用于处理接收到的消息。
构造函数
构造函数接受一个函数对象OnMessage
,用于处理接收到的UDP消息,以及可选的端口号和套接字文件描述符。
成员函数
-
Init()
: 初始化函数,用于创建套接字并将其绑定到指定的端口。-
首先,使用
socket
函数创建一个UDP套接字。 -
如果套接字创建失败,将记录错误日志并退出程序,返回
Socket_Err
错误代码。 -
使用
memset
函数清空sockaddr_in
结构体,并设置其端口、地址族和IP地址(INADDR_ANY
表示接受任何IPv4地址的连接)。 -
使用
bind
函数将套接字绑定到指定的端口和地址。 -
如果绑定失败,将记录错误日志并退出程序,返回
Bind_Err
错误代码。
-
-
Start()
: 启动服务器,开始接收和处理UDP消息。-
进入一个无限循环,等待接收UDP消息。
-
使用
recvfrom
函数接收来自客户端的UDP消息,并将其存储在buffer
中。 -
如果接收到消息,将调用
_OnMessage
函数对象处理该消息,并将响应发送回客户端。 -
注意,这里使用了
InetAddr
类(可能是自定义的)来打印或处理客户端的地址信息。
-
#pragma once
#include "ErrInfo.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
#include "NoCopy.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include <cstring>
#include <functional>
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
using func_t = std::function<std::string(std::string)>; // 定义了一个函数类型
static const uint16_t DefaultPort = 8888;
const static int DefaultFd = -1;
const static int DefaultSize = 1024;
class UdpServer : public NoCopy
{
public:
UdpServer(func_t OnMessage,const uint16_t &port = DefaultPort, const int &socket = DefaultFd)
: _port(port),
_socket(socket),
_OnMessage(OnMessage)
{
}
void Init()
{
// 创建套接字
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket < 0)
{
lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _socket);
// 填充服务端信息
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
local.sin_family = AF_INET;
// 将填充好的结构体绑定到操作系统中
socklen_t len = sizeof(local);
int ret = bind(_socket, (sockaddr *)&local, len);
if (ret != 0)
{
lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));
exit(Bind_Err);
}
}
void Start()
{
while (true)
{
char buffer[DefaultSize];
struct sockaddr_in peer; // 远端
socklen_t len = sizeof(peer);
//这里不能使用strlen
ssize_t ret = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len);
if (ret > 0)
{
InetAddr addr(peer);
buffer[ret] = 0;
// 处理消息
std::string response = _OnMessage(buffer);
// std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;
sendto(_socket, response.c_str(), response.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
private:
uint16_t _port;
int _socket;
func_t _OnMessage;
};
服务端的启动
这个程序是一个简单的UDP服务器示例,展示了如何处理来自客户端的消息,并执行一些基本的操作。
程序结构和功能
-
包含头文件:程序首先包含了多个头文件,包括自定义的"ErrInfo.hpp"、"UdpServer.hpp"以及其他标准库头文件,这些头文件提供了网络通信、字符串处理、标准输入输出等功能。
-
Usage函数:定义了一个
Usage
函数,当命令行参数不足时,会打印出正确的使用方法。 -
黑名单词汇:定义了一个
black_words
的字符串向量,里面包含了一些可能用于执行危险操作的命令词汇。 -
OnMessageDefault函数:这是一个默认的消息处理函数,它接收一个字符串作为请求,然后返回一个带有戏谑性附加语的请求字符串。
-
SafeCheck函数:这个函数接收一个命令字符串,并检查它是否包含黑名单中的任何词汇。如果包含,函数返回
false
,表示该命令可能是不安全的;否则返回true
。 -
ExecuteCommand函数:这个函数接收一个命令字符串,首先通过
SafeCheck
函数检查其安全性。如果命令不安全,返回一个警告字符串。如果安全,它尝试执行该命令,并捕获命令的输出,然后返回这个输出。如果执行过程中出现错误,也会返回相应的错误信息。 -
main函数:
-
参数检查:程序首先检查命令行参数的数量。如果参数数量不正确(不是2个,包括程序名和端口号),则调用
Usage
函数并退出程序。 -
创建UDP服务器:使用端口号创建一个
UdpServer
对象。这个服务器将使用OnMessageDefault
函数作为默认的消息处理函数,但代码中也有一行被注释掉的代码,显示可以使用ExecuteCommand
函数作为消息处理函数(这可能会带来安全风险)。 -
服务器初始化和启动:调用
Init
方法初始化服务器,然后调用Start
方法启动服务器。
-
#include "ErrInfo.hpp"
#include "UdpServer.hpp"
#include <cstdio>
#include <memory>
#include <vector>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n"
<< std::endl;
}
std::vector<std::string> black_words = {
"rm",
"unlink",
"cp",
"mv",
"chmod",
"exit",
"reboot",
"halt",
"shutdown",
"top",
"kill",
"dd",
"vim",
"vi",
"nano",
"man"};
std::string OnMessageDefault(std::string request)
{
return request + "[haha, got you!!]";
}
bool SafeCheck(std::string command)
{
for (auto &k : black_words)
{
std::size_t pos = command.find(k);
if (pos != std::string::npos)
return false;
}
return true;
}
// ls -a -l/ rm / tocuh
std::string ExecuteCommand(std::string command)
{
if (!SafeCheck(command))
return "bad man!!";
std::cout << "get a message: " << command << std::endl;
FILE *fp = popen(command.c_str(), "r");
if (fp == nullptr)
{
return "execute error, reason is unknown";
}
std::string response;
char buffer[1024];
while (true)
{
char *s = fgets(buffer, sizeof(buffer), fp);
if (!s)
break;
else
response += buffer;
}
pclose(fp);
return response.empty() ? "success" : response;
}
// ./udp_server 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(OnMessageDefault, port);
// std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ExecuteCommand, port);
usvr->Init();
usvr->Start();
return 0;
}
客户端
这是一个简单的UDP客户端程序,用C++编写。以下是对这个程序的详细介绍:
程序结构和功能
-
包含头文件:程序首先包含了多个头文件,这些头文件提供了网络通信、字符串处理、标准输入输出等功能。
-
Usage函数:定义了一个
Usage
函数,当命令行参数不足时,会打印出正确的使用方法,并退出程序。 -
main函数:
-
参数检查:程序首先检查命令行参数的数量。如果参数数量不正确(不是3个,包括程序名、服务器IP和端口),则调用
Usage
函数并退出。 -
创建套接字:使用
socket
函数创建一个UDP套接字。如果创建失败,则打印错误信息并退出。 -
服务器信息设置:使用
sockaddr_in
结构体来存储服务器的地址信息,包括IP地址和端口号。IP地址通过inet_addr
函数从字符串转换为网络字节序的格式。 - 主循环:进入一个无限循环,用于发送和接收数据。
-
发送数据:从标准输入读取用户输入的数据,并使用
sendto
函数发送到服务器。发送成功后,等待接收服务器的响应。 -
接收数据:使用
recvfrom
函数接收来自服务器的响应,并打印到标准输出。如果接收失败或接收到的数据量为0,则退出循环。
-
-
关闭套接字:在退出循环后,使用
close
函数关闭套接字。
-
程序特点
-
UDP通信:该程序使用UDP协议进行通信,UDP是无连接的,不保证数据的可靠传输,但传输速度快,适用于对实时性要求较高或数据量较小的场景。
-
命令行参数:程序通过命令行参数获取服务器的IP地址和端口号,提高了程序的灵活性。
-
错误处理:在关键步骤(如套接字创建、数据发送和接收)中进行了错误检查,并在出现错误时打印错误信息并退出程序。
-
用户交互:程序通过标准输入接收用户输入的数据,并将服务器的响应打印到标准输出,实现了基本的用户交互功能。
-
资源释放:在程序结束时关闭了套接字,避免了资源泄漏。
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <unistd.h>
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "create socket success: " << sock << std::endl;
// 2. client要不要进行bind? 一定要bind的!!但是,不需要显示bind,client会在首次发送数据的时候会自动进行bind
// 为什么?server端的端口号,一定是众所周知,不可改变的,client 需要 port,bind随机端口.
// 为什么?client会非常多.
// client 需要bind,但是不需要显示bind,让本地OS自动随机bind,选择随机端口号
// 2.1 填充一下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 inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
// 我们要发给谁呀?server
ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
if (n > 0)
{
char buffer[1024];
// 收消息
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len); // 一般建议都是要填的.
if (m > 0)
{
buffer[m] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else
break;
}
else
break;
}
close(sock);
return 0;
}
简单的udp聊天室服务器
服务端
类定义和成员变量
-
UdpServer
类继承自nocopy
类,意味着这个类的对象不能被复制,这通常是为了避免意外的对象复制可能引发的问题。 - 类中定义了几个私有成员变量:
-
_port
:服务器监听的端口号,默认为8888。 -
_sockfd
:套接字文件描述符,用于网络通信。 -
_online_user
:一个InetAddr
类型的向量,存储当前在线的用户地址信息。 -
_user_mutex
:互斥锁,用于保护_online_user
在多线程环境下的安全访问。
-
构造函数和析构函数
-
构造函数
UdpServer(uint16_t port = defaultport)
:初始化服务器的端口号,并设置默认的套接字文件描述符和初始化互斥锁。 -
析构函数
~UdpServer()
:销毁互斥锁。
成员函数
-
Init()
:初始化函数,用于创建套接字并将其绑定到指定的端口上。如果套接字创建或绑定失败,程序将记录错误信息并退出。成功创建套接字后,会启动线程池。 -
AddOnlineUser(InetAddr addr)
:将一个在线用户的地址添加到_online_user
列表中。这个函数是线程安全的,因为它在修改_online_user
之前会先锁定互斥锁。 -
Route(int sock, const std::string &message)
:将消息路由到所有在线的用户。这个函数也是线程安全的,同样使用了互斥锁。 -
Start()
:启动服务器的主循环,接收来自客户端的消息。当接收到消息时,会将发送者的地址添加到在线用户列表中,并将消息广播给所有在线用户。这个消息广播的任务被添加到线程池中异步执行。
线程安全和并发控制
-
类中使用了互斥锁(
pthread_mutex_t
)来保护在线用户列表_online_user
在多线程环境下的安全访问。当需要修改这个列表时,会先锁定互斥锁,确保同一时间只有一个线程可以修改这个列表。 -
使用线程池(
ThreadPool
)来异步处理消息广播的任务,提高服务器的并发处理能力。
总结
这个UdpServer
类是一个简单的UDP服务器实现,具有基本的并发控制和线程安全特性。它能够接收来自客户端的消息,并将消息广播给所有在线的用户。这个类使用了线程池来提高服务器的并发处理能力,并使用互斥锁来保护共享资源的安全访问。
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <pthread.h>
// #include <mutex>
// #include <condition_variable>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
using task_t = std::function<void()>;
// 聚焦在IO上
class UdpServer : public nocopy
{
public:
UdpServer(uint16_t port = defaultport) : _port(port), _sockfd(defaultfd)
{
pthread_mutex_init(&_user_mutex, nullptr);
}
void Init()
{
// 1. 创建socket,就是创建了文件细节
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);
// 2. 绑定,指定网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local)); // memset
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; // 0
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 4字节IP 2. 变成网络序列
// 结构体填完,设置到内核中了吗??没有
int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n != 0)
{
lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));
exit(Bind_Err);
}
ThreadPool<task_t>::GetInstance()->Start();
}
void AddOnlineUser(InetAddr addr)
{
LockGuard lockguard(&_user_mutex);
for (auto &user : _online_user)
{
if (addr == user)
return;
}
_online_user.push_back(addr);
lg.LogMessage(Debug, "%s:%d is add to onlineuser list...\n", addr.Ip().c_str(), addr.Port());
}
void Route(int sock, const std::string &message)
{
LockGuard lockguard(&_user_mutex);
for (auto &user : _online_user)
{
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&user.GetAddr(), sizeof(user.GetAddr()));
lg.LogMessage(Debug, "server send message to %s:%d, message: %s\n", user.Ip().c_str(), user.Port(), message.c_str());
}
}
void Start()
{
// 服务器永远不退出
char buffer[defaultsize];
for (;;)
{
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)
{
InetAddr addr(peer);
AddOnlineUser(addr);
buffer[n] = 0;
std::string message = "[";
message += addr.Ip();
message += ":";
message += std::to_string(addr.Port());
message += "]# ";
message += buffer;
task_t task = std::bind(&UdpServer::Route, this, _sockfd, message);
ThreadPool<task_t>::GetInstance()->Push(task);
}
}
}
~UdpServer()
{
pthread_mutex_destroy(&_user_mutex);
}
private:
uint16_t _port;
int _sockfd;
std::vector<InetAddr> _online_user; // 会被多个线程同时访问的
pthread_mutex_t _user_mutex;
};
服务端的启动
这是一个简单的UDP服务器示例,用于演示基本的网络通信和命令执行功能。
包含的头文件
程序首先包含了一些必要的头文件,这些头文件提供了程序所需的各种功能,如标准输入输出、内存管理、向量容器以及自定义的头文件(可能是为了网络通信和日志记录等)。
全局变量和函数
-
black_words
:这是一个包含“危险”命令的字符串向量。这些命令被认为是可能对系统造成危害的,因此程序会检查用户输入的命令是否包含这些“黑名单”词汇。 -
OnMessageDefault
:这是一个默认的消息处理函数,它简单地将接收到的消息加上一个固定的字符串后返回。 -
SafeCheck
:这个函数检查一个命令字符串是否包含black_words
中的任何词汇。如果包含,则返回false
,表示该命令不安全。 -
ExecuteCommand
:这个函数尝试执行一个传入的命令字符串,并返回命令的输出。在执行命令之前,它会先通过SafeCheck
函数检查命令的安全性。
main
函数
-
程序首先检查命令行参数的数量。如果参数数量不正确(即不是2个,包括程序自身的名称和一个端口号),则显示用法信息并退出。
-
然后,程序将第二个命令行参数(即端口号)转换为整数,并创建一个
UdpServer
对象。注意,这里使用了std::unique_ptr
来管理UdpServer
对象的生命周期,这是一种智能指针,可以在对象不再需要时自动删除它。 -
接下来,程序调用
UdpServer
对象的Init
方法来初始化服务器(例如,创建套接字并绑定到指定的端口)。 -
最后,程序调用
UdpServer
对象的Start
方法来启动服务器的主循环,等待并处理传入的UDP数据包。
#include "Comm.hpp"
#include "UdpServer.hpp"
#include <cstdio>
#include <memory>
#include <vector>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n"
<< std::endl;
}
std::vector<std::string> black_words = {
"rm",
"unlink",
"cp",
"mv",
"chmod",
"exit",
"reboot",
"halt",
"shutdown",
"top",
"kill",
"dd",
"vim",
"vi",
"nano",
"man"};
std::string OnMessageDefault(std::string request)
{
return request + "[haha, got you!!]";
}
bool SafeCheck(std::string command)
{
for (auto &k : black_words)
{
std::size_t pos = command.find(k);
if (pos != std::string::npos)
return false;
}
return true;
}
// ls -a -l/ rm / tocuh
std::string ExecuteCommand(std::string command)
{
if (!SafeCheck(command))
return "bad man!!";
std::cout << "get a message: " << command << std::endl;
FILE *fp = popen(command.c_str(), "r");
if (fp == nullptr)
{
return "execute error, reason is unknown";
}
std::string response;
char buffer[1024];
while (true)
{
char *s = fgets(buffer, sizeof(buffer), fp);
if (!s)
break;
else
response += buffer;
}
pclose(fp);
return response.empty() ? "success" : response;
}
// ./udp_server 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}
客户端
实现了一个简单的UDP客户端,可以同时进行数据的发送和接收
包含的头文件
程序首先包含了一系列必要的头文件,这些头文件为网络通信、线程操作、输入输出等提供了所需的功能。
Usage
函数
如果用户提供的命令行参数数量不正确,则此函数会打印出正确的使用方法。
ThreadData
类
这个类封装了线程所需的数据,包括一个套接字文件描述符 _sockfd
和一个 InetAddr
对象 _serveraddr
,该对象包含服务器的地址信息。
RecverRoutine
函数
这个函数是接收线程的主体函数。它在一个无限循环中不断地从套接字接收数据,并将数据打印到标准错误输出。如果接收失败,则跳出循环。
SenderRoutine
函数
这个函数是发送线程的主体函数。它也在一个无限循环中运行,提示用户输入要发送的数据,并将数据发送到服务器。如果发送失败,则打印错误消息。
main
函数
-
参数检查:首先检查命令行参数的数量。如果参数数量不正确,则打印使用方法并退出程序。
-
创建套接字:使用
socket
函数创建一个UDP套接字。 -
设置服务器地址:使用用户提供的IP地址和端口号设置服务器的地址结构。
- 创建并启动线程:
-
创建一个
ThreadData
对象,包含套接字和服务器地址信息。 -
创建两个线程:一个用于接收数据(
recver
),另一个用于发送数据(sender
)。 -
启动这两个线程。
-
-
等待线程结束:使用
Join
方法等待两个线程都执行完毕。 -
关闭套接字:在所有操作完成后,关闭套接字。
#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "InetAddr.hpp"
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
class ThreadData
{
public:
ThreadData(int sock, struct sockaddr_in &server) : _sockfd(sock), _serveraddr(server)
{
}
~ThreadData()
{
}
public:
int _sockfd;
InetAddr _serveraddr;
};
void RecverRoutine(ThreadData &td)
{
char buffer[4096];
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(td._sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len); // 一般建议都是要填的.
if (n > 0)
{
buffer[n] = 0;
std::cerr << buffer << std::endl; // 方便一会查看效果
}
else
break;
}
}
// 该线程只负责发消息
void SenderRoutine(ThreadData &td)
{
while (true)
{
// 我们要发的数据
std::string inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
auto server = td._serveraddr.GetAddr();
// 我们要发给谁呀?server
ssize_t n = sendto(td._sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
if (n <= 0)
std::cout << "send error" << std::endl;
}
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
// udp是全双工的。既可以读,也可以写,可以同时读写,不会多线程读写的问题
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "create socket success: " << sock << std::endl;
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());
ThreadData td(sock, server);
Thread<ThreadData> recver("recver", RecverRoutine, td);
Thread<ThreadData> sender("sender", SenderRoutine, td);
recver.Start();
sender.Start();
recver.Join();
sender.Join();
close(sock);
return 0;
}
依赖类
错误类型
#pragma once
#include<iostream>
enum{
Usage_Err = 1,
Socket_Err,
Bind_Err
};
日志类
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
enum
{
Screen = 10,
OneFile,
ClassFile
};
std::string LevelToString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Unknown";
}
}
const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";
class Log
{
public:
Log() : style(defaultstyle), filename(default_filename)
{
mkdir(logdir.c_str(), 0775);
}
void Enable(int sty) //
{
style = sty;
}
std::string TimeStampExLocalTime()
{
time_t currtime = time(nullptr);
struct tm *curr = localtime(&currtime);
char time_buffer[128];
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
curr->tm_hour, curr->tm_min, curr->tm_sec);
return time_buffer;
}
void WriteLogToOneFile(const std::string &logname, const std::string &message)
{
umask(0);
int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, message.c_str(), message.size());
close(fd);
}
void WriteLogToClassFile(const std::string &levelstr, const std::string &message)
{
std::string logname = logdir;
logname += "/";
logname += filename;
logname += levelstr;
WriteLogToOneFile(logname, message);
}
void WriteLog(const std::string &levelstr, const std::string &message)
{
switch (style)
{
case Screen:
std::cout << message;
break;
case OneFile:
WriteLogToClassFile("all", message);
break;
case ClassFile:
WriteLogToClassFile(levelstr, message);
break;
default:
break;
}
}
void LogMessage(int level, const char *format, ...) // 类C的一个日志接口
{
char leftbuffer[1024];
std::string levelstr = LevelToString(level);
std::string currtime = TimeStampExLocalTime();
std::string idstr = std::to_string(getpid());
char rightbuffer[1024];
va_list args; // char *, void *
va_start(args, format);
// args 指向了可变参数部分
vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
va_end(args); // args = nullptr;
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",
levelstr.c_str(), currtime.c_str(), idstr.c_str());
std::string loginfo = leftbuffer;
loginfo += rightbuffer;
WriteLog(levelstr, loginfo);
}
~Log() {}
private:
int style;
std::string filename;
};
Log lg;
class Conf
{
public:
Conf()
{
lg.Enable(ClassFile);
}
~Conf()
{
}
};
Conf conf;
防止拷贝类
#pragma once
#include<iostream>
class NoCopy
{
public:
NoCopy()
{}
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
~NoCopy()
{}
};
线程池
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <pthread.h>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"
static const int defaultnum = 10;
class ThreadData
{
public:
ThreadData(const std::string &name) : threadname(name)
{
}
~ThreadData()
{
}
public:
std::string threadname;
};
template <class T>
class ThreadPool
{
private:
ThreadPool(int thread_num = defaultnum) : _thread_num(thread_num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
// 构建指定个数的线程
for (int i = 0; i < _thread_num; i++)
{
// 待优化
std::string threadname = "thread-";
threadname += std::to_string(i + 1);
ThreadData td(threadname);
// Thread<ThreadData> t(threadname,
// std::bind(&ThreadPool<T>::ThreadRun, this, std::placeholders::_1), td);
// _threads.push_back(t);
_threads.emplace_back(threadname,
std::bind(&ThreadPool<T>::ThreadRun, this,
std::placeholders::_1),
td);
lg.LogMessage(Info, "%s is created...\n", threadname.c_str());
}
}
ThreadPool(const ThreadPool<T> &tp) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T>) = delete;
public:
// 有线程安全问题的
static ThreadPool<T> *GetInstance()
{
if (instance == nullptr)
{
LockGuard lockguard(&sig_lock);
if (instance == nullptr)
{
lg.LogMessage(Info, "创建单例成功...\n");
instance = new ThreadPool<T>();
}
}
return instance;
}
bool Start()
{
// 启动
for (auto &thread : _threads)
{
thread.Start();
lg.LogMessage(Info, "%s is running ...\n", thread.ThreadName().c_str());
}
return true;
}
void ThreadWait(const ThreadData &td)
{
lg.LogMessage(Debug, "no task, %s is sleeping...\n", td.threadname.c_str());
pthread_cond_wait(&_cond, &_mutex);
}
void ThreadWakeup()
{
pthread_cond_signal(&_cond);
}
void ThreadRun(ThreadData &td)
{
while (true)
{
// checkSelf()
// checkSelf();
// 取任务
T t;
{
LockGuard lockguard(&_mutex);
while (_q.empty())
{
ThreadWait(td);
lg.LogMessage(Debug, "thread %s is wakeup\n", td.threadname.c_str());
}
t = _q.front();
_q.pop();
}
// 处理任务
t();
// lg.LogMessage(Debug, "%s handler task %s done, result is : %s\n",
// td.threadname, t.PrintTask().c_str(), t.PrintResult().c_str());
}
}
void Push(T &in)
{
LockGuard lockguard(&_mutex);
_q.push(in);
ThreadWakeup();
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
// for debug
void Wait()
{
for (auto &thread : _threads)
{
thread.Join();
}
}
private:
std::queue<T> _q;
std::vector<Thread<ThreadData>> _threads;
int _thread_num;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static ThreadPool<T> *instance;
static pthread_mutex_t sig_lock;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER;
线程类
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
// 设计方的视角
//typedef std::function<void()> func_t;
template<class T>
using func_t = std::function<void(T&)>;
template<class T>
class Thread
{
public:
Thread(const std::string &threadname, func_t<T> func, T &data)
:_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
{}
static void *ThreadRoutine(void *args) // 类内方法,
{
// (void)args; // 仅仅是为了防止编译器有告警
Thread *ts = static_cast<Thread *>(args);
ts->_func(ts->_data);
return nullptr;
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
if(n == 0)
{
_isrunning = true;
return true;
}
else return false;
}
bool Join()
{
if(!_isrunning) return true;
int n = pthread_join(_tid, nullptr);
if(n == 0)
{
_isrunning = false;
return true;
}
return false;
}
std::string ThreadName()
{
return _threadname;
}
bool IsRunning()
{
return _isrunning;
}
~Thread()
{}
private:
pthread_t _tid;
std::string _threadname;
bool _isrunning;
func_t<T> _func;
T _data;
};
网络信息类
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr
{
public:
InetAddr(struct sockaddr_in &addr):_addr(addr)
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
std::string Ip() {return _ip;}
uint16_t Port() {return _port;};
std::string PrintDebug()
{
std::string info = _ip;
info += ":";
info += std::to_string(_port); // "127.0.0.1:4444"
return info;
}
const struct sockaddr_in& GetAddr()
{
return _addr;
}
bool operator == (const InetAddr&addr)
{
//other code
return this->_ip == addr._ip && this->_port == addr._port;
}
~InetAddr(){}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
守护锁类
#pragma once
#include <pthread.h>
// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
Mutex(pthread_mutex_t *lock):_lock(lock)
{}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock): _mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};