本章重点
能够实现一个简单的udp客户端/服务器;
1.创建套接字
我们把服务器封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字。
⭐参数说明:
- domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或 AF_INET6(IPv6)。
- type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
- protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
这里我们使用我们之前写的Log.hpp文件来方便观察输出信息。
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
当我们在进行初始化服务器创建套接字时,就是调用socket函数创建套接字,创建套接字时我们需要填入的协议家族就是AF_INET
,因为我们要进行的是网络通信,而我们需要的服务类型就是SOCK_DGRAM
,因为我们现在编写的UDP服务器是面向数据报的,而第三个参数之间设置为0即可。
enum
{
SOCKET_ERR = 1
};
class UdpServer
{
public:
UdpServer()
{}
void Init()
{
// 1.创建udp socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
log.operator()(Fatal, "sockfd create error: %d", _sockfd);
exit(SOCKET_ERR);
}
log(Info, "socket create success, sockfd: %d", _sockfd); // 3
}
~UdpServer() {}
private:
int _sockfd; // 网络文件描述符
};
我们来运行一下:
2.绑定端口号
⭐注意:编写的是UDP协议的服务器
现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个通道,用户并不知道将来并不知道是要将数据传给哪个服务器,此时客户端服务器还没有与服务端服务器关联起来,所以我们就要提前讲服务器的ip地址,端口号和套接字绑定起来。
⭐参数说明:
- sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
- addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:传入的addr结构体的长度。
⭐返回值说明:
- 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。
套接字创建完毕后我们就需要进行绑定了,但在绑定之前我们需要先定义一个struct sockaddr_in结构,将对应的网络属性信息填充到该结构当中。由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空。
然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。需要注意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons函数将端口号转为网络序列。
此外,由于网络当中传输的是整数IP,我们如何快速的将字符串IP和整数IP快速转化呢?
那么上面的这个需要我们自己来实现嘛?那网络用起来也太繁琐了吧!不要紧,操作系统为我们提供了方法:inet_addr,能将字符串分隔的地址转成网络序列的的四字节整数,我们需要调用inet_addr函数将字符串IP转换成整数IP,然后再将转换后的整数IP进行设置。
这里有一点细节需要注意:
因此我们这里需要使用in_add的成员s_addr才能将我们的类型进行很好的匹配。
我们的struct sockaddr_in local在哪呢?它在进程地址空间中用户的栈上面,也就是在用户区,我们给结构体填入所有的内容都是在用户区填入的,但是socket套接字是系统调用,在内核区,也就是说此时我们并没有和内核中的套接字相关联,所以我们此时就要绑定bind.
由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将struct sockaddr_in*强转为struct sockaddr*类型后再进行传入。
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport, const string &ip = defaultip)
: _port(port), _ip(ip)
{
}
void Init()
{
// 1.创建udp socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
log.operator()(Fatal, "sockfd create error: %d", _sockfd);
exit(-1);
}
log(Info, "socket create success, sockfd: %d", _sockfd); // 3
// 2.绑定端口号
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET;
local.sin_port = htons(_port);//需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
//1.string -> uint_32_t
//2.必须是网络序列的
local.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
if(n < 0) //绑定失败
{
log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
log(Info,"bind create success");
}
void Run() {}
~UdpServer() {}
private:
int _sockfd; // 网络文件描述符
string _ip; // 服务器的ip
uint16_t _port; // 服务器进程的端口号
};
我们来运行一下:
3.服务器运行并接收处理请求
接下里我们就要让我们的服务器跑起来,作为一款服务器,它应该是24小时始终运行的。服务器实际上就是在周而复始的为我们提供某种服务,服务器之所以称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是一个死循环代码。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据,怎么读取呢?UDP服务器不是面向字节流的所以不能用read,它是面向数据报的,所以要使用recvfrom
⭐参数说明:
- sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
- buf:读取数据的存放位置。
- len:期望读取数据的字节数。
- flags:读取的方式。一般设置为0,表示阻塞读取。
- src_addr:用户端网络相关的属性信息,包括协议家族、IP地址、端口号等,输出型参数。
- addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
⭐返回值说明:
- 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
⭐注意:
- 由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到用户端网络相关的属性信息,包括IP地址和端口号等。
- 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
- 由于recvfrom函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。
4.服务器发送请求结果
发送数据的函数叫做sendto,该函数的函数原型如下:
⭐参数说明:
- sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
- buf:待写入数据的存放位置。
- len:期望写入数据的字节数。
- flags:写入的方式。一般设置为0,表示阻塞写入。
- dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等,输入型参数。
- addrlen:传入dest_addr结构体的长度,输入型参数。
⭐返回值说明:
- 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
⭐注意:
- 由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
- 由于sendto函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport, const string &ip = defaultip)
: _port(port), _ip(ip)
{
}
void Init()
{
// 1.创建udp socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
log.operator()(Fatal, "sockfd create error: %d", _sockfd);
exit(-1);
}
log(Info, "socket create success, sockfd: %d", _sockfd); // 3
// 2.绑定端口号
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET;
local.sin_port = htons(_port);//需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
//1.string -> uint_32_t
//2.必须是网络序列的
local.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
if(n < 0) //绑定失败
{
log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
log(Info,"bind create success");
}
void Run()
{
// 服务器一直在运行
_isrunning = true;
char inbuffer[1024];
while(_isrunning)
{
// 获取用户端的ip,端口号,用户发送的请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
if(n < 0)
{
log(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = '\0'; //当作字符串来看
// 数据的处理
string info = inbuffer;
string echo_string = "sever echo#" + info;
// 数据发送给用户
sendto(_sockfd, echo_string.c_str(), echo_string.size(),0,(const struct sockaddr*)&client, len);
}
}
~UdpServer() {}
private:
int _sockfd; // 网络文件描述符
string _ip; // 服务器的ip
uint16_t _port; // 服务器进程的端口号
bool _isrunning; // 服务器是否在运行
};
此时程序运行起来了,但是服务器到底有没有启动呢?我们可以通过netstat指令查看
netstat -nlup
是一个在类Unix系统(如Linux和macOS)中常用的命令,用于查看网络连接状态。这个命令结合了几个选项来提供特定的输出信息。下面是对每个选项的解释:
-n
:表示以数字形式显示IP地址和端口号,而不是尝试将其解析为主机名和服务名称。-l
:显示正在监听的套接字。-u
:显示UDP协议的连接信息。-p
:显示与每个连接或监听端口关联的进程ID和进程名称。
并且此时能看到服务器的ip是0.0.0.0,端口号是8080
我们现在使用的是轻量级服务器,我们可以尝试一下绑定我们的远端服务器的ip,看看此时有什么效果。
此时绑定失败了,为什么呢?云服务器禁止直接绑定公网ip,云服务的ip地址可能有多个,如果你指绑定一个,其他的ip就收不到请求。怎么解决呢?将绑定操作中的IP地址设为0,代表“任意IP地址”。这意味着相应的网络服务将会监听并接受来自该主机上任何IP地址的所有网络接口上的连接请求。bind(IP:0):凡是发给我这台主机的数据,我们都要根据端口号向上交付,这种方式叫做任意地址绑定,所以我们刚刚绑定的ip就可以这样写啦!
从此以后,凡是发给我这台主机的数据,可以忽略ip地址,只需要使用端口号向上交付。然后我们在恢复之前的ip地址的形式,随后我们刚刚给我们端口号设置的是8080,现在我们设置成80,结果咋样呢?
提示没有权限,好,我们提权sudo
此时的端口号通过提权就能绑定成功,但是为什么刚刚8080的端口号就不需要提权呢?[0,1023]:系统内定的端口号, 一般都要有固定的应用层协议使用,http: 80 https: 443 mysq: 3606...,期望我们绑定的端口号都在1024以上。所以我们的端口号该怎么处理呢?使用我们的命令行参数决定绑定哪一个端口号。
void Usage(string proc)
{
cout << "\n\tUsage: " << proc << " port[1024+]" << endl;
}
// ./udpserver port
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = stoi(argv[1]);
unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run();
return 0;
}
运行结果:
那我们总得看效果吧,光让服务器跑起来没啥用处啊,接下里我们就来写一个客户端。
5.编写客户端
1.本地网络通信
⭐细节问题:
直接来看代码:
void Usage(string proc)
{
cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
// 我怎么知道服务器是谁呀 - 命令行参数来解决
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 清空结构体
server.sin_family = AF_INET;
server.sin_port = htons(serverport); // 需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
// 1.string -> uint_32_t
// 2.必须是网络序列的
server.sin_addr.s_addr = inet_addr(serverip.c_str());
socklen_t len = sizeof(server);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) // 创建套接字失败
{
cout << "sockfd create error";
exit(1);
}
log(Info, "socket create success, sockfd: %d", sockfd); // 3
// 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
// 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
// 系统什么时候给我bind呢?首次发送数据的时候
string message;
char buffer[1024];
while (true)
{
cout << "Please Enter: ";
getline(cin, message);
// 发送信息给服务端
sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);
// 接收服务端的信息
// recvform输出型参数
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = '\0';
cout << buffer << endl;
}
}
close(sockfd);
return 0;
}
⭐注意:我们要传入云服务器的私有ip,不是我们的公网ip哟!后面解释!!!
我们来看看运行结果:
此时你只要拥有了客户端的这个代码,ip和端口号,那么你就可以给我这台机器随便发消息,服务器都能收到!!!
但是现在我们不想在服务器处理用户发过来的数据,我们在用户来处理数据,这份做法本质是让代码进行分层,方便维护。
std::string Handler(const std::string &str)
{
std::string res = "Server get a message: ";
res += str;
std::cout << res << std::endl;
return res;
}
同时这里用户还可以随意设置自己想要的结果,所以我们现在就可以写一点好玩的。我们可以把传入的字符串当作指令来处理。
popen
是一个在 C 语言中使用的函数,用于通过创建一个管道(pipe)来启动一个子进程,并执行一个shell命令。这个函数允许父进程与子进程之间进行输入/输出通信。popen
函数的主要特点和用途包括:
创建管道:它首先创建一个管道,这是一个半双工的通信机制,允许数据在两个进程间单向流动。
启动子进程:接着,通过
fork()
系统调用创建一个子进程。子进程继承了父进程的管道描述符。执行命令:在子进程中,使用
execl()
或相似的函数来执行一个shell命令。这使得父进程能够间接地执行系统命令或外部程序。返回文件指针:
popen
函数返回一个FILE *
类型的文件指针。如果命令执行成功,这个文件指针可以用作fread()
、fwrite()
、fgets()
等标准I/O函数的参数,从而读取子进程的输出的数据。关闭管道:当完成通信后,应该使用
pclose()
函数来关闭管道并等待子进程结束。pclose()
也会返回子进程的退出状态。
std::string ExcuteCommand(const std::string &cmd)
{
// popen创建一个管道并来启动一个子进程执行shell命令
// 随后通过管道将执行的命令给父进程
// 父进程可以通过打开文件来看执行结果
FILE *fp = popen(cmd.c_str(), "r");
if(nullptr == fp)
{
perror("popen");
return "error";
}
// 拿执行结果
std::string result;
char buffer[4096];
while(true)
{
char *ok = fgets(buffer, sizeof(buffer), fp);
if(ok == nullptr) break;
result += buffer;
}
pclose(fp);
return result;
}
此时我们就可以根据字符串执行相应的指令了,但是我们还是要检测一下指令的输入,万一客户端来个删库的命令哪咋办,所以我们要处理一下,保证指令是一个安全的指令。
bool SafeCheck(const string &cmd)
{
vector<string> key_word = {
"rm",
"mv",
"cp",
"kill",
"sudo",
"unlink",
"uninstall",
"yum",
"top",
"while"};
for (auto e : key_word)
{
auto pos = cmd.find(e);
if (pos != string::npos)
{
return false;
}
}
return true;
}
std::string ExcuteCommand(const std::string &cmd)
{
if(!SafeCheck(cmd)) //安全检查
return "Bad man";
// popen创建一个管道并来启动一个子进程执行shell命令
// 随后通过管道将执行的命令给父进程
// 父进程可以通过打开文件来看执行结果
FILE *fp = popen(cmd.c_str(), "r");
if (nullptr == fp)
{
perror("popen");
return "error";
}
std::string result;
char buffer[4096];
while (true)
{
char *ok = fgets(buffer, sizeof(buffer), fp);
if (ok == nullptr)
break;
result += buffer;
}
pclose(fp);
return result;
}
之前都是使用的云服务器的ip地址,现在我们学习一个新的ip地址,127.0.0.1是一个特殊的IP地址,用于回环测试。它也被称为本地主机或回环地址。当一个设备向这个IP地址发送数据时,数据并不会离开该设备,而是在设备自身的网络堆栈中进行循环。这通常用于在不涉及外部网络的情况下,在本地机器上测试网络应用程序和服务。现在我们在构造的时候传入127.0.0.1的ip地址。
⭐127.0.0.1:本地环回地址,通常用它来进行cs的测试
其实上面的用户端就类似于我们的xshell,我们每次登录的时候都需要ip地址去连接远端服务器,我们在xshell里面输入的字符串,服务器会接收到并处理好返回给我们。
完整代码展示:
makefile:
.PHONY:all
all:udpserver udpclient
udpserver:main.cpp
g++ -o $@ $^ -std=c++11
udpclient:udpClient.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpserver udpclient
udpClient.cpp:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(string proc)
{
cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
// 我怎么知道服务器是谁呀 - 命令行参数来解决
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 清空结构体
server.sin_family = AF_INET;
server.sin_port = htons(serverport); // 需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
// 1.string -> uint_32_t
// 2.必须是网络序列的
server.sin_addr.s_addr = inet_addr(serverip.c_str());
socklen_t len = sizeof(server);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) // 创建套接字失败
{
cout << "sockfd create error";
exit(1);
}
// 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
// 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
// 系统什么时候给我bind呢?首次发送数据的时候
string message;
char buffer[1024];
while (true)
{
cout << "Please Enter: ";
getline(cin, message);
// 发送信息给服务端
sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&server, len);
// 接收服务端的信息
// recvform输出型参数
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = '\0';
cout << buffer << endl;
}
}
close(sockfd);
return 0;
}
udpServer.hpp:
#pragma once
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <unistd.h>
#include <sys/types.h>
#include <cstdlib>
#include <functional>
using namespace std;
using func_t = function<string(const string&)>;
// 类似于
// typedef function<string(const string&)> func_t;
Log lg;
enum
{
SOCKET_ERR = 1,
BIND_ERR = 2
};
uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport, const string &ip = defaultip)
: _port(port), _ip(ip), _sockfd(0),_isrunning(false)
{
}
void Init()
{
// 1.创建udp socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
lg.operator()(Fatal, "sockfd create error: %d", _sockfd);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", _sockfd); // 3
// 2.绑定端口号
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET;
local.sin_port = htons(_port);//需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
//1.string -> uint_32_t
//2.必须是网络序列的
local.sin_addr.s_addr = inet_addr(_ip.c_str());
//local.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));
if(n < 0) //绑定失败
{
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info,"bind create success");
}
void Run(func_t func)
{
// 服务器一直在运行
_isrunning = true;
char inbuffer[1024];
while(_isrunning)
{
// 获取用户端的ip,端口号,用户发送的请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, inbuffer, 1023, 0, (struct sockaddr*)&client, &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = '\0'; //当作字符串来看
// 数据的处理
string info = inbuffer;
string echo_string = func(info);
// cout << echo_string << endl;
// 数据发送给用户
sendto(_sockfd, echo_string.c_str(), echo_string.size(),0,(const struct sockaddr*)&client, len);
}
}
~UdpServer()
{
if(_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd; // 网络文件描述符
string _ip; // 服务器的ip 任意地址绑定
uint16_t _port; // 服务器进程的端口号
bool _isrunning; // 服务器是否在运行
};
main.cpp:
#include "udpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>
// "120.78.126.148" 点分十进制字符串风格的IP地址
// 4个字节ip地址,但是用户不关心
void Usage(string proc)
{
cout << "\n\tUsage: " << proc << " port[1024+]" << endl;
}
std::string Handler(const std::string &str)
{
std::string res = "Server get a message: ";
res += str;
std::cout << res << std::endl;
return res;
}
bool SafeCheck(const string &cmd)
{
vector<string> key_word = {
"rm",
"mv",
"cp",
"kill",
"sudo",
"unlink",
"uninstall",
"yum",
"top",
"while"};
for (auto e : key_word)
{
auto pos = cmd.find(e);
if (pos != string::npos)
{
return false;
}
}
return true;
}
std::string ExcuteCommand(const std::string &cmd)
{
if (!SafeCheck(cmd))// 安全检查
return "Bad man";
// popen创建一个管道并来启动一个子进程执行shell命令
// 随后通过管道将执行的命令给父进程
// 父进程可以通过打开文件来看执行结果
FILE *fp = popen(cmd.c_str(), "r");
if (nullptr == fp)
{
perror("popen");
return "error";
}
std::string result;
char buffer[4096];
while (true)
{
char *ok = fgets(buffer, sizeof(buffer), fp);
if (ok == nullptr)
break;
result += buffer;
}
pclose(fp);
return result;
}
// ./udpserver port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = stoi(argv[1]);
unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run(Handler);
return 0;
}
2.跨平台网络通信
linux的套接字接口和windows的套接字接口一样吗?虽然操作系统是不同的,但是它们都遵守网络标准,底层的网络协议栈是相同的,所以它们的套接字接口都是一样的,所以两个不同的平台也可以进行网络通信,那咱们来试试!!!
#pragma warning(disable:4996) //inet_addr,不安全,直接禁掉
#include <iostream>
#include <winsock2.h>
#include <Windows.h>
#include <string>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
#define IP ""172.17.40.254""
#define PORT 8080
int main(int argc, char* argv[])
{
//初始化网络环境
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
cout << "WSAStartup failed" << endl;
return -1;
}
// 申明一个网络地址信息的结构体,保存服务器的地址信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = inet_addr(IP);
//建立一个udp的socket
SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);
if (sockClient < 0)
{
cout << "create socket failed" << endl;
return -1;
}
string message;
char buffer[1024];
while (true)
{
cout << "Please Enter: ";
getline(cin, message);
// 发送信息给服务端
sendto(sockClient, message.c_str(), (int)message.size(), 0, (const struct sockaddr*)&server, sizeof(server));
// 接收服务端的信息
// recvform输出型参数
struct sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(sockClient, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if (s > 0)
{
buffer[s] = '\0';
cout << buffer << endl;
}
}
//关闭sockClient
closesocket(sockClient);
//清理网络环境
WSACleanup();
system("pause");
return 0;
}
那咱们来运行一下哈
3.简易的群聊系统
首先我们就需要获取到用户的端口号和ip地址,我们可以在服务器哪里进行获取。
此时就成功获取了用户那端的ip和端口号,此时我只有一个主机,服务端和客户端都在同一台主机上,所以ip地址都一样,这在群聊中相当于自己发信息给自己。
作为服务器,服务器不仅收到了用户发送的信息,还接收了到了用户的ip地址,因此我们就可以通过ip地址来标识用户,所以我们可以维护一个登录列表,看看当前有多少用户登录了服务器,直接写代码。
#pragma once
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <unistd.h>
#include <sys/types.h>
#include <cstdlib>
#include <functional>
#include <unordered_map>
using namespace std;
using func_t = function<string(const string &, const string &, uint16_t &)>;
// 类似于
// typedef function<string(const string&)> func_t;
Log lg;
enum
{
SOCKET_ERR = 1,
BIND_ERR = 2
};
uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport, const string &ip = defaultip)
: _port(port), _ip(ip), _sockfd(0), _isrunning(false)
{
}
void Init()
{
// 1.创建udp socket
// udp 的socket是全双工的,允许被同时读写的
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) // 创建套接字失败
{
lg.operator()(Fatal, "sockfd create error: %d", _sockfd);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", _sockfd); // 3
// 2.绑定端口号
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清空结构体
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
// 1.string -> uint_32_t
// 2.必须是网络序列的
local.sin_addr.s_addr = inet_addr(_ip.c_str());
// local.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
int n = bind(_sockfd, (const struct sockaddr *)&local, sizeof(local));
if (n < 0) // 绑定失败
{
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind create success");
}
void CheckUser(const struct sockaddr_in &client, const string& clientip, uint16_t& clientport)
{
auto iter = _onlineuser.find(clientip); // 找ip
if (iter == _onlineuser.end())
{
// 添加用户 - 入群
_onlineuser.insert({clientip, client});
cout << "[" << clientip << ":" << clientport << "] add to online user" << endl;
}
else
{
return;
}
}
void Broadcast(const string& info, const string& clientip, uint16_t& clientport)
{
for(const auto& user: _onlineuser)
{
// 数据的处理
std::string message = "[";
message += clientip;
message += ":";
message += std::to_string(clientport);
message += "]# ";
message += info;
socklen_t len = sizeof(user.second);
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
}
}
void Run()
{
// 服务器一直在运行
_isrunning = true;
char inbuffer[1024];
while (_isrunning)
{
// 获取用户端的ip,端口号,用户发送的请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, inbuffer, 1023, 0, (struct sockaddr *)&client, &len);
if (n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = '\0'; // 当作字符串来看
// 拿到用户端的端口号和ip地址
uint16_t clientport = ntohs(client.sin_port);
string clientip = inet_ntoa(client.sin_addr);
// 判断是否是一个新用户
CheckUser(client, clientip, clientport);
// 数据发送给所有用户
string info = inbuffer;
Broadcast(info, clientip, clientport);
}
}
~UdpServer()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd; // 网络文件描述符
string _ip; // 服务器的ip 任意地址绑定
uint16_t _port; // 服务器进程的端口号
bool _isrunning; // 服务器是否在运行
unordered_map<string, struct sockaddr_in> _onlineuser;
};
我们来看看运行结果:
但是我们的客户端是一个单进程,向服务器发信息和从服务器收到信息都在同一个进程,并且我们是先发送信息的,所以就有一点尴尬,我们任意一个用户只有发一个信息才能收到其他用户发出的信息,如果它不发信息,getline便会阻塞住,代码就不能继续向后执行,尽管此时别的服务器给我发送了信息,但是我们的代码还在getline那里阻塞者呢,我们还没执行到从服务器接收信息的代码,所以此时不能收到信息,但是群聊的时候,我们不发消息也能收到其他人的信息呀!此时我们就需要多线程来解决,向服务器发信息和从服务器收到信息使用多线程,让它俩互不干扰。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;
struct ThreadData
{
struct sockaddr_in server;
int sockfd;
};
void Usage(string proc)
{
cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}
void *recv_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
char buffer[1024];
while (true)
{
// 接收服务端的信息
// recvform输出型参数
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = '\0';
cout << buffer << endl;
}
}
}
void *send_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
string message;
socklen_t len = sizeof(td->server);
while (true)
{
cout << "Please Enter: ";
getline(cin, message);
// 发送信息给服务端
sendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&td->server, len);
}
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
struct ThreadData td;
// 我怎么知道服务器是谁呀 - 命令行参数来解决
bzero(&td.server, sizeof(td.server)); // 清空结构体
td.server.sin_family = AF_INET;
td.server.sin_port = htons(serverport); // 需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
// 1.string -> uint_32_t
// 2.必须是网络序列的
td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0) // 创建套接字失败
{
cout << "sockfd create error";
exit(1);
}
// 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
// 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
// 系统什么时候给我bind呢?首次发送数据的时候
pthread_t recvr, sender;
pthread_create(&recvr, nullptr, recv_message, &td);
pthread_create(&sender, nullptr, send_message, &td);
pthread_join(recvr, nullptr);
pthread_join(sender, nullptr);
close(td.sockfd);
return 0;
}
运行一下:
但是此时我们的输入和输出都在一个窗口,看着比较混乱,我们可以多开几个终端来让它们分开,linux下一些皆文件,我们的终端也是文件。
此时我们可以发现我们这个终端其实就是dev/pts目录下的0号文件,所以我们可以借助这个文件向终端输入内容,那我们怎么通过代码来执行呢?
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string terminal = "/dev/pts/0";
using namespace std;
int main()
{
int fd = open(terminal.c_str(), O_WRONLY);
if (fd < 0)
{
std::cerr << "open terminal error" << std::endl;
exit(1);
}
//cout << fd << endl;
dup2(fd, 1); // 重定向标准输出到/dev/pts/0
printf("hello world\n");
close(fd);
return 0;
}
运行一下:
紧接着我们立马把它应用到客户端,让客户端的输入和输出在两个终端。
Treminal.hpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string terminal = "/dev/pts/0";
using namespace std;
int OpenTerminal()
{
int fd = open(terminal.c_str(), O_WRONLY);
if (fd < 0)
{
std::cerr << "open terminal error" << std::endl;
exit(1);
}
//cout << fd << endl;
// 由于线程的文件描述符是共享的,所以我换一个
dup2(fd, 2); // 重定向标准错误到/dev/pts/0
return 0;
}
udpClient.cpp
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"
using namespace std;
struct ThreadData
{
struct sockaddr_in server;
int sockfd;
};
void Usage(string proc)
{
cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}
void *recv_message(void *args)
{
OpenTerminal();
ThreadData *td = static_cast<ThreadData *>(args);
char buffer[1024];
while (true)
{
// 接收服务端的信息
// recvform输出型参数
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = '\0';
cerr << buffer << endl; //使用标准错误
// 此时我们的标准错误就已经重定向到终端
}
}
}
void *send_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
string message;
socklen_t len = sizeof(td->server);
while (true)
{
cout << "Please Enter: ";
getline(cin, message);
// 发送信息给服务端
sendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&td->server, len);
}
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
struct ThreadData td;
// 我怎么知道服务器是谁呀 - 命令行参数来解决
bzero(&td.server, sizeof(td.server)); // 清空结构体
td.server.sin_family = AF_INET;
td.server.sin_port = htons(serverport); // 需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
// 1.string -> uint_32_t
// 2.必须是网络序列的
td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0) // 创建套接字失败
{
cout << "sockfd create error";
exit(1);
}
// 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
// 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
// 系统什么时候给我bind呢?首次发送数据的时候
pthread_t recvr, sender;
pthread_create(&recvr, nullptr, recv_message, &td);
pthread_create(&sender, nullptr, send_message, &td);
pthread_join(recvr, nullptr);
pthread_join(sender, nullptr);
close(td.sockfd);
return 0;
}
运行结果:
此时就完成了简单的群聊系统。还有一种简单的方法,我们上面的用户发出信息是使用的标准输出,而收到信息是标准错误,两个文件描述符是不同的,所以我们可以直接将用户端输入ip和端口号的地方标准错误重定向到我们的终端下即可。
./udpclient 172.17.40.254 8080 2 > /dev/pts/0
现在我们再来优化一下,每个用户在一旦访问我们的服务器的时候,我们可以设置一条欢迎语。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"
#include <string.h>
using namespace std;
struct ThreadData
{
struct sockaddr_in server;
int sockfd;
std::string serverip;
};
void Usage(string proc)
{
cout << "\n\tUsage: " << proc << " serverip serverport" << endl;
}
void *recv_message(void *args)
{
OpenTerminal();
ThreadData *td = static_cast<ThreadData *>(args);
char buffer[1024];
while (true)
{
memset(buffer, 0, sizeof(buffer));
// 接收服务端的信息
// recvform输出型参数
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = '\0';
cerr << buffer << endl; // 使用标准错误
// 此时我们的标准错误就已经重定向到终端
}
}
}
void *send_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
string message;
socklen_t len = sizeof(td->server);
// 发送欢迎语
std::string welcome = td->serverip;
welcome += " comming...";
sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (struct sockaddr *)&(td->server), len);
while (true)
{
cout << "Please Enter: ";
getline(cin, message);
// 发送信息给服务端
sendto(td->sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&td->server, len);
}
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
struct ThreadData td;
// 我怎么知道服务器是谁呀 - 命令行参数来解决
bzero(&td.server, sizeof(td.server)); // 清空结构体
td.server.sin_family = AF_INET;
td.server.sin_port = htons(serverport); // 需要保证我的端口号是网络字节序列(大端),因为该端口号是要给对方发送的
// 1.string -> uint_32_t
// 2.必须是网络序列的
td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0) // 创建套接字失败
{
cout << "sockfd create error";
exit(1);
}
td.serverip = serverip;
// 客户端也需要有ip和端口号,这样服务器才能找到用户,返回用户的请求
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 如果用户自行绑定,有可能绑定同一个端口号,导致应用无法运行
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 未来是用户给服务器,一旦绑定,用户的端口号是先发给服务端,此时服务器就知道是谁了!
// 但是server的port需要唯一确定,用户要访问服务器,随机变化导致第一天还可以,后面无法运行
// 系统什么时候给我bind呢?首次发送数据的时候
pthread_t recvr, sender;
pthread_create(&recvr, nullptr, recv_message, &td);
pthread_create(&sender, nullptr, send_message, &td);
pthread_join(recvr, nullptr);
pthread_join(sender, nullptr);
close(td.sockfd);
return 0;
}
其实这个谁谁conmming,就相当于谁谁已经加入群聊啦!!!