UDP网络编程
V1版本 - Echo Server
简单的回显服务器和客户端代码,我们购买的云服务器充当服务器,对应的Linux充当客户端,发一句,回一句,通常可以用来做网络接口的简单测试!
备注:代码中会用到地址转换函数,参考接下来的章节。
我们既然要进行网络通信,而且Linux下一切接文件,将来要进行网络收发的时候需要将网络文件打开,网络文件这个概念我们暂时还是有点模糊的,我们可以理解为至少要将网卡打开。
我们之前打开文件使用open就可以打开,但是在网络中是使用 --- socket(系统调用)
我们先来封装一下服务端的代码:UdpServer.cc
UdpServer.cc
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
#include "Mutex.hpp"
#include "Log.hpp"
using namespace LogModule;
const int defaultfd = -1;
class UdpServer
{
public:
UdpServer(const std::string &ip, uint16_t port)
: _sockfd(defaultfd), _port(port), _ip(ip), _isrunning(false)
{
}
~UdpServer()
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success, sockfd = " << _sockfd;
// 2. 绑定socket信息,主要是ip和端口,ip(比较特殊,我们后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;
// memset(&local, 0, sizeof(local)); // 防止干扰
bzero(&local, sizeof(local)); // 防止干扰
local.sin_family = AF_INET;
// 我会不会把我的IP地址和端口号发送给对方?
// 肯定会的!也就决定了IP信息和端口信息一定要发送到网络上
// 我们就需要将本地存储序列格式转换成网络序列格式,因为网络字节序统一是大端
local.sin_port = htons(_port);
// IP同上,但是我们的IP是点分十进制的string风格,需要将IP转为4字节的网络字节序
// inet_addr()函数就是用来将点分十进制的IP地址转为4字节网络字节序的,小端转大端是比较容易的,我们来说说IP转4字节网络字节序的过程
// 我们了一定义一个结构体ip,内容是char part1/2/3/4(地址从低到高),对于"188.168.1.1",以点为分隔符,拆成4个子字符串,在转化成数字,然后将数字转化成4字节网络字节序
// s_addr成员变量就是用来存储4字节网络字节序的
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 还有别的做法
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket failed";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success, port = " << _port;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1.收消息
int s = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
buffer[s] = '\0';
LOG(LogLevel::DEBUG) << "buffer = " << buffer; // 1.消息内容 2.谁发的?
// 2.发消息
std::string echo_string = "server say@ ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
private:
int _sockfd;
uint16_t _port;
std::string _ip; // 用的是字符串风格,点分十进制的IP地址
bool _isrunning;
};
下面是代码实现过程中的细节补充:
socket
socket
是一种系统调用,用于创建和操作网络套接字,是网络通信的端点,它为应用程序提供了与网络协议栈交互的接口,使得程序能够实现数据的发送和接收,从而进行网络通信。以下是对其的详细介绍:
原型 :在 Linux 系统中,socket 函数的原型为:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int family, int type, int protocol);
其中,family
指定协议族,type
指定套接字类型,protocol
指定协议号。
常见参数取值 :
family : 协议族 :AF_INET
表示 IPv4 地址家族,AF_INET6
表示 IPv6,AF_UNIX
或 AF_LOCAL
表示Unix域套接字,用于同一台机器上的进程间通信。
type : 套接字类型 :SOCK_STREAM
表示面向连接的 TCP 流式套接字,提供可靠、有序的数据传输;SOCK_DGRAM
表示无连接的 UDP 数据报套接字,适用于传输对实时性要求高但对可靠性要求不那么严格的场景;SOCK_RAW
表示原始套接字,允许对较低层协议直接访问。
protovol : 协议号 :通常可以指定为 0,此时系统会根据套接字类型选择默认的协议。例如,对于 SOCK_STREAM
类型,系统会选择 TCP 协议;对于 SOCK_DGRAM
类型,系统会选择 UDP 协议。
创建套接字 :当调用 socket()
系统调用时,内核会根据指定的协议族、套接字类型和协议号,创建一个新的套接字对象,并为其分配内存空间和相关的资源,同时生成一个唯一的套接字文件描述符,作为该套接字的标识返回给调用者。(后面的建立-传输-关闭,我们逐步展开)
连接建立 :对于面向连接的套接字,如 TCP 套接字,客户端需要使用 connect()
系统调用主动与服务器端建立连接。服务器端则需要先使用 bind()
系统调用将套接字与本地的 IP 地址和端口号绑定,然后使用 listen()
系统调用使套接字进入监听状态,等待客户端的连接请求。当客户端发起连接请求时,服务器端通过 accept()
系统调用接受请求,从而建立可靠的网络连接。
数据传输 :连接建立后,客户端和服务器端就可以通过 send()
、recv()
等系统调用进行数据的发送和接收。在底层,发送的数据会被操作系统拆分成一个个网络数据包,每个数据包包含目标计算机的 IP 地址、端口号、数据等信息,然后通过网络协议栈进行传输。接收方的网络协议栈接收到数据包后,会将其重组并传递给目标套接字,再由套接字将数据传递给应用程序。
连接关闭 :数据传输完成后,双方需要关闭套接字连接,以释放相关的资源,可以使用 close()
系统调用关闭套接字。
bind
我们创建套接字成功之后,就需要绑定socket信息,主要是ip和端口,ip比较特殊,后面会说明,这时候,我们需要使用到一个系统调用 --- bind
bind
是一个用于将套接字(socket)与特定的网络地址和端口号绑定的系统调用。它是网络编程中非常重要的一步,通常在服务器端程序中使用。通过 bind
,可以指定套接字所要监听的网络接口以及端口号,从而实现网络通信的目的。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd
:套接字文件描述符,由socket()
调用返回。 -
addr
:指向存储套接字地址信息的结构体的指针,通常是struct sockaddr
结构体。 -
addrlen
:套接字地址结构体的长度。
当程序调用 bind
系统调用时,操作系统会根据套接字文件描述符 sockfd
找到对应的套接字结构体,然后将该结构体中的地址信息设置为 addr
所指向的地址结构体中的值,并将地址结构体的大小设置为 addrlen
。如果地址结构体中的端口号为 0,则操作系统会在调用 bind
时随机分配一个可用的端口号。
在 Linux 操作系统中,bind
系统调用的底层实现是通过调用 sys_bind
函数来实现的。该函数会在内核中为套接字分配一段内存空间,并将套接字结构体中的地址信息保存到该内存空间中,同时将该套接字加入到相应的协议控制块(PCB)中,以便进行进一步的操作。
我们代码继续往下写就需要填充sockaddr_in结构体,既然需要填充sockaddr_in结构体,我们就需要先来认识一下sockaddr_in结构体:
sockaddr_in结构体
sockaddr_in结构体主要有3个字段:
- 第一个是地址类型,也叫做协议家族
- 第二个是port,就是要绑定的端口号是什么
- 第三个,你要绑定,那么你的服务器所对应的IP地址是什么
8位填充,我们清零就可以。
我们下面一张图是相关源代码:
"##"的作用是将'##'两边的符号,合并为一个符号:
这个宏传入的sin_将来就会和family拼起来,构成一个符号sa_family_t是一个数据类型,其实就是一个无符号短整数(unsigned short int)
我们将该结构体填入对应的IP和端口号还不算真正的写入到内核当中,这只是在栈上开辟的,所以我们需要使用bind系统调用!
上面过程已经完成了 UdpServer
类的初始化部分,包括创建套接字和绑定本地 IP 地址与端口。接下来,我们需要在 Start()
方法中实现 UDP 服务器的核心逻辑,即接收和处理客户端发送的数据。
需要使用 recvfrom()
函数来接收客户端发送的数据。recvfrom()
函数不仅可以接收数据,还可以获取发送数据的客户端的地址信息。
recvfrom
recvfrom()
是一个用于接收 UDP 数据报的系统调用。它不仅可以接收数据,还可以获取发送数据的客户端的地址信息。以下是对 recvfrom()
的详细介绍:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
:套接字文件描述符,由 socket()
调用返回。这个套接字应该已经绑定到本地地址,并且处于接收状态。
buf
:用于存储接收到数据的缓冲区。缓冲区的大小由 len
参数指定。
len
:指定缓冲区的大小,以字节为单位。这个参数限制了单次调用 recvfrom()
最多可以接收的数据量。
flags
:用于指定接收数据时的选项。常见的标志包括:
0
:普通接收,无特殊选项。(代表阻塞式IO:如果对方不发数据,该函数(进程)就会一直阻塞,等同于scanf)MSG_PEEK
:窥探模式,数据被接收但保留在接收队列中,后续接收调用仍可获取这些数据。MSG_WAITALL
:等待直到接收完指定长度的数据再返回。
src_addr
:用于存储发送数据的客户端的地址信息。通常是一个指向 struct sockaddr_in
的指针。(我们先实现的是服务端的代码,服务端在读取客户端信息时,会拿到用户发送到的数据(buf),但是服务端是想要知道该消息是谁发送的,将来作为一款服务器,所对应的客户端可不止一个,所有的客户端都可以向服务器发送消息,服务器只要知道客户端的套接字信息,即IP地址和端口号,就可以具体知道发送数据的客户端是谁了,这个src_addr其实是一个输出型参数,将来要求我们用户,必须传递一个struct sockaddr_in
结果)
addrlen
:指定 src_addr
的大小。在调用前,需要初始化为 src_addr
缓冲区的大小。调用后,会更新为实际存储的地址信息的长度。(既是输入型参数,也是输出型参数)
我们可以接收到客户端发送的消息之后,作为Echo Server,我们需要是发消息作为处理消息的行为,我们使用系统调用sendto来实现。
sendto()
是一个用于发送 UDP 数据报的系统调用。它允许向指定的地址发送数据,而无需事先建立连接。以下是对 sendto()
的详细介绍:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd
:套接字文件描述符,由 socket()
调用返回。这个套接字应该已经绑定到本地地址。(所以我们可以得到一个结论:UDP sockfd,既可以读,又可以写,UDP通信其实是全双工的)
buf
:包含要发送数据的缓冲区。数据从这个缓冲区中读取并发送到网络。
len
:指定要发送的数据长度,以字节为单位。实际发送的数据量可能小于这个值,具体取决于套接字缓冲区和网络状况。
flags
:用于指定发送数据时的选项。常见的标志包括:
0
:普通发送,无特殊选项。MSG_CONFIRM
:发送数据并请求确认。MSG_DONTROUTE
:跳过路由表,直接发送数据。MSG_DONTWAIT
:在非阻塞模式下发送数据,即使缓冲区满也不阻塞。
dest_addr
:指向目标地址的指针,通常是一个 struct sockaddr_in
结构体。指定数据发送的目标地址和端口。(要知道数据要发送给谁,这是一个输入型参数)
addrlen
:指定 dest_addr
的大小。通常设置为 sizeof(struct sockaddr_in)
。
这样,客户端直接向服务器发送消息,服务器直接就可以接收到,这就是UDP不面向连接!!!
自此,我们服务端的代码就基本搞定了。
UdpServer.cc
接下来,我们的服务端就需要创建自己的服务器,我们按照下面格式启动服务器:
./udpserver [ip] [port]
我们就需要使用命令行参数!
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// ./udpserver [ip] [port]
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Usage: " << argv[0] << " [ip] [port]" << std::endl;
return 1;
}
std::string ip = argv[1];
uint16_t port = std::atoi(argv[2]);
Enable_Console_Log_Strategy(); // 往显示器上打印消息
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);
usvr->Init();
usvr->Start();
return 0;
}
我们下面来测试一下:
我们发现bind失败,这个我们下面谈论。但是我们的套接字创建成功了,且sockfd是3,证明是文件描述符,因为0/1/2被占用。
UdpClient.cc
下面我们赶快来实现客户端,因为客户端比较简单,这里就不做封装了:
作为客户端,要访问一个目标服务器,需要知道什么?
在我们的日常生活中,网络请求的发起方永远都是客户端,服务器来进行被动处理,客户端给服务器发送消息,必须知道服务器的IP地址和端口号!不过作为一个客户端,该怎么知道服务端的IP地址和端口号呢?!
抖音客户端怎么知道访问字节的服务器呢?怎么知道字节的IP地址和端口号呢?不要忘了!客户端和服务器是一家公司写的!!!(简单来说,就是使用的APP内置了对应的服务器的IP和端口)
2.本地的IP和端口是什么?要不要和上面的 "文件"关联呢? 问题:client要不要bind?client要不要显示的bind?为什么?
client肯定是需要bind的,用IP地址和端口号来保证自己在全网的唯一性!但是client往往不需要我们调用bind这个函数了,在client首次发送消息(调用sendto)的时候,OS会自动给client进行bind,客户端的IP地址,OS是知道的,端口号采用随机端口号的机制进行分配。我们知道一个端口号只能被一个端口号bind。然而,我们如果真的要让客户端进行显示的bind,也是可以的,但是我们特别不推荐这么做,因为我们如果让客户端绑定了一个例如【666】号端口,这样我们就绑死了,客户端不仅有字节的产品,京东的产品....我们在自己手机上大大小小,各种各样的APP在我们手机上安装着,如果我们自己写了一个死绑端口为666的客户端程序,这时候我们的客户端好没启动,但是抖音启动,随机绑到了端口号666,这样就导致我的客户端就启动不了了。所以我们采用随机端口号是为了保证客户端之间的端口号不发生冲突!
所以client端的端口号并不重要,只需要保证其client可以在全网中具有唯一性就可以了,但是为什么server端需要显示的bind呢?
因为服务端是要被许多客户端进行访问的目标,所以服务端的IP地址,尤其是端口,必须是总所周知,且不易改变的! (就像110这种号码是不会轻易改变的,但是个人手机号就可以一直改!)
后面我们client创建套接字后,只需要向服务端发送消息就可以了。
#include <iostream>
#include <string>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// ./udpclient [server_ip] [server_port]
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Usage: " << argv[0] << " [server_ip] [server_port]" << std::endl;
return 1;
}
std::string server_ip = argv[1];
int server_port = atoi(argv[2]);
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket failed." << std::endl;
return 2;
}
// 2.本地的IP和端口是什么?要不要和上面的"文件"关联呢?
// 问题:client要不要bind?client要不要显示的bind?为什么?
// 填写服务器端信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while (true)
{
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.length(), 0, (struct sockaddr *)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (m > 0)
{
buffer[m] = '\0';
std::cout << buffer << std::endl;
}
}
return 0;
}
现在我们的客户端就可以创建套接字,基于服务器信息,向目标服务器发送消息了。
我们下面要来完整的跑代码,我云服务器的IP为XXXXX,端口号不能直绑定【0,1023】中的,一般是8080,但是目前创建套接字是成功的,但是bind失败了:
与此同时,我们在进行绑定的时候,IP地址还有一个:127.0.0.1
本地环回,这个IP地址我们用来服务器绑定,将来在正常客户端,服务端通信的时候,有一个特点:要求客户端和服务器要在同一台机器上,表明我们是本地通信,也就是说client发送的数据,不会被推送到网络,而是在OS内部,转一圈直接交给对应的服务端,经常用来进行网络代码的测试!(可能网也有问题,如果内部能通过,那就是网的问题了)
那我们来看看使用这个IP能否bind成功:
我们发现,这时候创建套接字成功了,bind也成功了,接下来,我们继续测试:
其实我们将这个IP换成内网IP都是可以的,甚至一个使用内网IP,一个使用本地环回!但是有点问题。自己测试一下就知道了!
所以:上面结论是:
- bind公网IP --- 不能(我们使用云服务器,采用ifconfig查看IP的时候,根本没有公网IP,主要原因是公网IP并没有配置到我们的服务器上)(云服务器提供商一般不会将公网 IP 直接配置到服务器的本地网络接口上。这是为了网络安全和管理的需要,云服务器通常位于私有网络中,通过 NAT 或其他网络设备与公网进行通信,而不是直接将公网 IP 绑定到服务器的网络接口上。因此,使用
ifconfig
查看时,只能看到服务器的私有 IP 地址,而看不到公网 IP。)(所以,我们对应往后的实现套接字时,无法使用公网IP直接bind) - bind127.0.0.1 || 内网 --- OK
- server bind 内网IP,但是使用127.0.0.1访问(反着同样) --- 访问不了(如果我们显示的进行地址bind,client未来访问的时候,就必须使用server端bind的地址信息)(IP要匹配)
既然公网IP无法bind了,剩下的IP又要匹配,那我们该如何实现跨网络访问呢?
我们一般在做服务器开发的时候,我们所对应的服务器端,不建议手动bind特定的IP,不仅仅是因为公网不让我们去bind,也不仅仅是说我们bing了一个IP就只能用这个IP了,我们不建议使用:
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 还有别的做法
而是使用:
local.sin_addr.s_addr = INADDR_ANY; // 这里用的是INADDR_ANY,表示接收所有IP地址,这样可以接收来自任何IP的消息
这个宏其实就是0,这里用的是INADDR_ANY,表示接收所有IP地址,这样可以接收来自任何IP的消息了!(也就是凡是访问这一台机器的,端口号时8080的,报文全交给这台机器就可以了),所以,我们的服务器端的代码就可以将IP全部注释掉了:
更新后的UdpServer.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
#include "Mutex.hpp"
#include "Log.hpp"
using namespace LogModule;
const int defaultfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port)
: _sockfd(defaultfd), _port(port), _isrunning(false)
{
}
~UdpServer()
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success, sockfd = " << _sockfd;
// 2. 绑定socket信息,主要是ip和端口,ip(比较特殊,我们后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;
// memset(&local, 0, sizeof(local)); // 防止干扰
bzero(&local, sizeof(local)); // 防止干扰
local.sin_family = AF_INET;
// 我会不会把我的IP地址和端口号发送给对方?
// 肯定会的!也就决定了IP信息和端口信息一定要发送到网络上
// 我们就需要将本地存储序列格式转换成网络序列格式,因为网络字节序统一是大端
local.sin_port = htons(_port);
// IP同上,但是我们的IP是点分十进制的string风格,需要将IP转为4字节的网络字节序
// inet_addr()函数就是用来将点分十进制的IP地址转为4字节网络字节序的,小端转大端是比较容易的,我们来说说IP转4字节网络字节序的过程
// 我们了一定义一个结构体ip,内容是char part1/2/3/4(地址从低到高),对于"188.168.1.1",以点为分隔符,拆成4个子字符串,在转化成数字,然后将数字转化成4字节网络字节序
// s_addr成员变量就是用来存储4字节网络字节序的
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 还有别的做法
local.sin_addr.s_addr = INADDR_ANY; // 这里用的是INADDR_ANY,表示接收所有IP地址,这样可以接收来自任何IP的消息
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket failed";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success, port = " << _port;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1.收消息
int s = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
buffer[s] = '\0';
LOG(LogLevel::DEBUG) << "buffer = " << buffer; // 1.消息内容 2.谁发的?
// 2.发消息
std::string echo_string = "server say@ ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 用的是字符串风格,点分十进制的IP地址
bool _isrunning;
};
更新后的UdpServer.cc
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// ./udpserver [port]
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Usage: " << argv[0] << "[port]" << std::endl;
return 1;
}
// std::string ip = argv[1];
uint16_t port = std::atoi(argv[1]);
Enable_Console_Log_Strategy(); // 往显示器上打印消息
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
usvr->Init();
usvr->Start();
return 0;
}
测试:我们就可以实现:
在 UDP 服务器的开发过程中,我们常常面临一个问题:如何让服务器能够接收来自不同网络(如公网、私网)的数据呢?今天,我找到了一个非常实用的解决方案。
当我们使用 INADDR_ANY
这个宏来设置服务器的绑定 IP 地址时,服务器就能够监听所有网络接口上的数据了(与服务器进行bind的IP的客户端)。在代码中,我们只需要将 local.sin_addr.s_addr
设置为 INADDR_ANY
,这样服务器就可以接收来自任何有效 IP 地址(与服务器有关联的IP地址)的数据,无论是公网 IP、私有 IP 还是本地环回 IP。这就可以实现数据跨网络传输了。
后面,只要将client程序让被人使用(注意使用-static进行静态编译链接),使用./udpclient [服务器对应公网IP] ,就可以实现跨网络传输了:
但是我们想要知道这些消息到底是谁发的,我们就可以将客户端的套接字信息打印出来:利用输出型参数:我们对上述代码部分稍作增加:
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1.收消息
int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
int peer_port = ntohs(peer.sin_port); // 网络字节序转主机字节序,获取对方端口号
std::string peer_ip = inet_ntoa(peer.sin_addr); // 网络字节序转主机字节序,获取对方IP地址
LOG(LogLevel::DEBUG) << "recvfrom peer_ip = " << peer_ip << ", peer_port = " << peer_port;
buffer[s] = '\0';
LOG(LogLevel::DEBUG) << "buffer = " << buffer; // 1.消息内容 2.谁发的?
// 2.发消息
std::string echo_string = "server say@ ";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
现象:
自此,我们就实现了Echo Server的编写了!
上面我们实现的时Linux之间的套接字网络通信,其实Windows和Linux的套接字的接口是一样的!,因为网络部分是一样的!我们后面会见到!😜
下面,我们来对代码进行进一步的调整,扩展
我们客户端给服务器发消息,不就是想让我们的服务器帮我做对应的处理工作嘛,所以,我们下面对代码进行层状设计:
优化后的UdpServer.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
#include <functional>
#include "Log.hpp"
using namespace LogModule;
using func_t = std::function<std::string(const std::string &)>;
const int defaultfd = -1;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func) // 构造函数,传入端口号和回调函数
: _sockfd(defaultfd), _port(port), _isrunning(false), _func(func)
{
}
~UdpServer()
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "create socket success, sockfd = " << _sockfd;
// 2. 绑定socket信息,主要是ip和端口,ip(比较特殊,我们后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;
// memset(&local, 0, sizeof(local)); // 防止干扰
bzero(&local, sizeof(local)); // 防止干扰
local.sin_family = AF_INET;
// 我会不会把我的IP地址和端口号发送给对方?
// 肯定会的!也就决定了IP信息和端口信息一定要发送到网络上
// 我们就需要将本地存储序列格式转换成网络序列格式,因为网络字节序统一是大端
local.sin_port = htons(_port);
// IP同上,但是我们的IP是点分十进制的string风格,需要将IP转为4字节的网络字节序
// inet_addr()函数就是用来将点分十进制的IP地址转为4字节网络字节序的,小端转大端是比较容易的,我们来说说IP转4字节网络字节序的过程
// 我们了一定义一个结构体ip,内容是char part1/2/3/4(地址从低到高),对于"188.168.1.1",以点为分隔符,拆成4个子字符串,在转化成数字,然后将数字转化成4字节网络字节序
// s_addr成员变量就是用来存储4字节网络字节序的
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 还有别的做法
local.sin_addr.s_addr = INADDR_ANY; // 这里用的是INADDR_ANY,表示接收所有IP地址,这样可以接收来自任何IP的消息
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket failed";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket success, port = " << _port;
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1.收消息
int s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (s > 0)
{
int peer_port = ntohs(peer.sin_port); // 网络字节序转主机字节序,获取对方端口号
std::string peer_ip = inet_ntoa(peer.sin_addr); // 网络字节序转主机字节序,获取对方IP地址
LOG(LogLevel::DEBUG) << "recvfrom peer_ip = " << peer_ip << ", peer_port = " << peer_port;
buffer[s] = '\0';
std::string result = _func(buffer); // 调用回调函数进行处理----分层处理
// LOG(LogLevel::DEBUG) << "buffer = " << buffer; // 1.消息内容 2.谁发的?
// 2.发消息
// std::string echo_string = "server say@ ";
// echo_string += buffer;
// sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 用的是字符串风格,点分十进制的IP地址
bool _isrunning;
func_t _func; // 服务器的回调函数,用来进行对数据进行处理
};
上层处理:UdpServer.cc
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
// 翻译系统,字符串当成英文单词
// ./udpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
usvr->Init();
usvr->Start();
return 0;
}
测试结果:
我们上面的只是一个测试,但是我们实现分层了,未来只需要对上层修改,封装,就可以实现翻译任务,群聊等等任务,实现代码的解耦!!!我们下一篇来实现!!!