目录
服务端类UdpServer的模拟实现
服务端类UdpServer的成员变量
服务端类UdpServer的构造函数、初始化函数initServer、析构函数
服务端类UdpServer的start函数
服务端类UdpServer的整体代码(即udp_server.h文件的整体代码)
基于服务端类UdpServer模拟实现的服务端
udp_server.cc文件的整体代码
客户端的模拟实现
udp_client.cc文件的整体代码
基于UDP协议的网络服务器的测试
在本地中测试
在网络中测试
bind绑定INADDR_ANY后的服务端(即udp_server.cc文件的整体代码)
bind绑定INADDR_ANY后的服务端类UdpServer的整体代码(即udp_server.h文件的整体代码)
(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)
一个服务器是需要绑定ip和端口号的,不然其他机器找不到该服务器,所以成员中肯定是有_ip和_port的。
在网络中,发信息需要一个通信通道,这个通道为【当前进程--->sockfd指向的文件的文件缓冲区(即分配给该文件的某块内存)--->内核缓冲区--->网卡--->网络--->对方的网卡--->对方的内核缓冲区--->对方的sockfd指向的文件的文件缓冲区--->对方的进程】;接收信息需要一个通信通道,这个通道为【对方进程--->对方进程的sockfd指向的文件的文件缓冲区(即分配给该文件的某块内存)--->对方的内核缓冲区--->对方的网卡--->网络--->当前主机的网卡--->当前主机的内核缓冲区--->当前进程的sockfd指向的文件的文件缓冲区--->当前的进程】。根据前面的理论,可以看出这里在网络中,服务端进程和客户端进程通信肯定是需要通过socket文件的,需要该文件作为通信通道中的一环,所以服务端中肯定是需要一个指向该socket文件的文件描述符_sock的。
根据上面的理论,服务端类UdpServer的成员变量如下。
#include
using namespace std;
#include//提供bzero函数
#include//提供智能指针的库
#include//提供atoi函数、exit函数
#include //提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include//系统库,提供socket编程需要的相关的接口
#include//系统库,提供socket编程需要的相关的接口
#include//提供sockaddr_in结构体
#include//提供sockaddr_in结构体
class UdpServer
{
public:
private:
//一个服务器是需要ip和端口号的,不然其他机器找不到该服务器
string _ip;
uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。
int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};
(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)
思路如下:
结合上面的理论,服务端类UdpServer的构造函数、初始化函数initServer、析构函数的代码如下。
#include
using namespace std;
#include//提供bzero函数
#include//提供智能指针的库
#include//提供atoi函数、exit函数
#include //提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include//系统库,提供socket编程需要的相关的接口
#include//系统库,提供socket编程需要的相关的接口
#include//提供sockaddr_in结构体
#include//提供sockaddr_in结构体
class UdpServer
{
public:
UdpServer(uint16_t port, string ip):_ip(ip),_port(port)
{}
void initServer()
{
//首先创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if(_sock == -1)
{
cout<<"创建套接字失败"<= 0)
close(_sock);
}
private:
//一个服务器是需要ip和端口号的,不然其他机器找不到该服务器
string _ip;
uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。
int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};
(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)
对于我们当前模拟实现的服务器类UdpServer,如果想让服务器跑起来,则调用其成员函数的顺序是【UdpServer类的构造函数--->UdpServer类的初始化函数initServer--->UdpServer类的start函数--->UdpServer类的析构函数】,可以看到start函数是在初始化函数之后的,走完初始化函数后,已经完成了socket文件的创建、将【当前进程】和【转化成网络字节序后的某个ip地址和某个端口port】进行绑定,剩下的工作就是start函数需要完成的了,需要做的事情为:
结合上面的理论,服务端类UdpServer的start函数的代码如下。
#include
using namespace std;
#include//提供bzero函数
#include//提供智能指针的库
#include//提供atoi函数、exit函数
#include //提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include//系统库,提供socket编程需要的相关的接口
#include//系统库,提供socket编程需要的相关的接口
#include//提供sockaddr_in结构体
#include//提供sockaddr_in结构体
class UdpServer
{
public:
void Start()
{
char c[1024];//当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,服务端读取到的客户端的信息就存放在数组c中
sockaddr_in client_info;//纯输出型参数,作为参数传给recvfrom函数后即可获取客户端的ip和port(需要获取的原因是:当前Start函数是服务端调用的函数,用于获取、分析、处理客户端的信息并反馈给客户端,所以是需要客户端的ip和port的),注意因为这里是作为输出型参数,所以原则上不必调用bzero函数初始化。
socklen_t client_info_len = sizeof(client_info);//输入输出型参数,作为参数传给recvfrom函数。输入时的值为:client_info对象所占的空间大小/输出时的值为:实际填充进client_info的数据的大小
while(1)
{
ssize_t size = recvfrom(_sock, (void*)c, sizeof(c), 0,(sockaddr*)&client_info, &client_info_len);//从客户端读取消息。函数需要的参数flag设置为0即可,不必关心其含义。recvfrom的返回值为实际读取到的字节个数。
if (size > 0)
{
//走到这里服务端已经读取到了客户端发过来的信息
c[size] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码
uint16_t port = ntohs(client_info.sin_port);//把从网络中来的port从网络字节序转化成主机字节序
string ip = (inet_ntoa(client_info.sin_addr));//会自动把从网络中来的uint16_t整形的ip地址先从网络字节序转化成主机字节序,然后将主机字节序的整形ip转化string类型的ip
printf("[%s][%d]#:%s\n",ip.c_str(), port, c);
}
/*
走到这里开始处理从客户端读到
的信息,这需要经过一段的时间。
*/
//把从客户端读取到的信息处理完毕后,向客户端发出反馈信息
sendto(_sock, c, strlen(c), 0, (sockaddr*)&client_info, client_info_len);
}
}
private:
//一个服务器是需要ip和端口号的,不然其他机器找不到该服务器
string _ip;
uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。
int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};
下面是整个udp_server.h文件的代码。
#include
using namespace std;
#include//提供bzero函数
#include//提供智能指针的库
#include//提供atoi函数、exit函数
#include //提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include//系统库,提供socket编程需要的相关的接口
#include//系统库,提供socket编程需要的相关的接口
#include//提供sockaddr_in结构体
#include//提供sockaddr_in结构体
class UdpServer
{
public:
UdpServer(uint16_t port, string ip):_ip(ip),_port(port)
{}
~UdpServer()
{
if(_sock >= 0)
close(_sock);
}
void initServer()
{
//首先创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if(_sock == -1)
{
cout<<"创建套接字失败"< 0)
{
//走到这里服务端已经读取到了客户端发过来的信息
c[size] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码
uint16_t port = ntohs(client_info.sin_port);//把从网络中来的port从网络字节序转化成主机字节序
string ip = (inet_ntoa(client_info.sin_addr));//会自动把从网络中来的uint16_t整形的ip地址先从网络字节序转化成主机字节序,然后将主机字节序的整形ip转化string类型的ip
printf("[%s][%d]#:%s\n",ip.c_str(), port, c);
}
/*
走到这里开始处理从客户端读到
的信息,这需要经过一段的时间。
*/
//把从客户端读取到的信息处理完毕后,向客户端发出反馈信息
sendto(_sock, c, strlen(c), 0, (sockaddr*)&client_info, client_info_len);
}
}
private:
//一个服务器是需要ip和端口号的,不然其他机器找不到该服务器
string _ip;
uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。
int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};
udp_server.cc文件的整体代码如下。逻辑非常简单,从命令行中获取到传给服务端进程的ip和port后,通过它们构造出在上文中模拟实现出的UdpServer类的对象,然后通过该对象调用UdpServer类的成员函数initServer和Start,这样服务端进程就跑起来了,即服务器就跑起来了。(注意udp_server.h的代码是在上文中模拟实现出来的)
#include"udp_server.h"
void usage(char* c)
{
printf("Usage:%s ip port\n", c);
}
//以后运行server进程的方式是输入命令:./udp_server ip port
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(1);
}
string ip = argv[1];
uint16_t port = atoi(argv[2]);
unique_ptr up(new UdpServer(port, ip));
up->initServer();
up->Start();
return 0;
}
(tips:如果对以下内容感到疑惑,建议结合<<套接字socket编程的基础知识点>>一文阅读)
客户端的大逻辑就是设置一个循环,每次循环都要【向服务端进程发送信息,然后接收服务端的反馈信息】。发送信息前也和服务端一样,也需要调用socket函数创建套接字文件、也需要调用相关接口将对端的ip和port从主机字节序转化成网络字节序。需要进行这些操作的原因在讲解客户端时都已经说过了,这里不再赘述。
还有一些笔者对于客户端的实现的补充说明,这些内容都在下面代码的注释中,请结合代码进行思考。
#include
using namespace std;
#include//提供bzero函数
#include //提供close函数
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include//系统库,提供socket编程需要的相关的接口
#include//系统库,提供socket编程需要的相关的接口
#include//提供sockaddr_in结构体
#include//提供sockaddr_in结构体
void usage(char* c)
{
printf("usage:%s ip port\n", c);
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
//首先创建套接字文件,即创建通信通道文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
cout<<"创建套接字失败"< 0)
{
c[s] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码
printf("服务端返回给我(客户端)的信息为:%s\n", c);
}
}
close(sock);
return 0;
}
将上文中编写好的udp_server.cc和udp_client.cc文件编译好后,直接在两个ssh渠道中分别运行它们,在运行时要传入命令行参数:
这样一来,客户端进程client和服务端进程server收发数据时就只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中。
测试结果如下(左半部分是服务端、右半部分的上面是客户端,右半部分的下面是一个用于检验本机各端口的网络连接情况的指令)。可以看到结果是符合我们的预期的,客户端向服务端发送信息后,服务端能收到该信息,并能将信息处理后再给客户端发送反馈信息。
问题:如何证明服务端中收到的信息是客户端发过来的呢?
答案:通过上图红线连接的内容可以看出在服务端的shell界面中打印出的端口号52455就是客户端绑定的端口号(该端口号是客户端第一次向服务端发送信息时OS随机分配的),也就证明了在服务端中受到的信息就是客户端发过来的。
现在我们已经通过了本地测试,接下来就需要进行网络测试了,那是不是直接让服务端绑定我的公网IP,此时这个服务端就能够被外网访问了呢?
理论上是这样的,但在实际bind绑定的过程中会出现问题,如下图所示,是无法bind的,其原因在<<套接字socket编程的基础知识点>>一文中已经说过了,详情请见该篇文章的内容。
问题:那当前的代码如何才能进行网络测试呢?
答案:让服务端进程bind绑定INADDR_ANY即可,其原因在<<套接字socket编程的基础知识点>>一文中也已经说过了,截图如下。
既然要让服务端进程bind绑定INADDR_ANY,那我们就要把上文中模拟实现的服务端类UdpServer(即udp_server.h文件)的代码和服务端(即udp_server.cc文件)的代码稍作修改。哪些地方需要修改呢?非常简单,如下:
根据上面的理论进行修改后,bind绑定INADDR_ANY后的服务端的代码如下。
#include"udp_server.h"
void usage(char* c)
{
printf("Usage:%s port\n", c);
}
//以后运行server进程的方式是输入命令:./udp_server port
int main(int argc, char* argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
//string ip = argv[1];
uint16_t port = atoi(argv[1]);
unique_ptr up(new UdpServer(port));
up->initServer();
up->Start();
return 0;
}
根据上面的理论进行修改后,bind绑定INADDR_ANY后的服务端类UdpServer的整体代码如下。
#include
using namespace std;
#include//提供bzero函数
#include//提供智能指针的库
#include//提供atoi函数、exit函数
#include //提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include//系统库,提供socket编程需要的相关的接口
#include//系统库,提供socket编程需要的相关的接口
#include//提供sockaddr_in结构体
#include//提供sockaddr_in结构体
class UdpServer
{
public:
UdpServer(uint16_t port, string ip = ""):_ip(ip),_port(port)
{}
~UdpServer()
{
if(_sock >= 0)
close(_sock);
}
void initServer()
{
//首先创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if(_sock == -1)
{
cout<<"创建套接字失败"< 0)
{
//走到这里服务端已经读取到了客户端发过来的信息
c[size] = 0;//因为在设计客户端与服务端通信时,设计的逻辑是双方都只发送C语言字符串,所以通信通道中的信息就全是C语言字符串,所以这里需要在读到的数据的末尾设置0,防止打印时出现乱码
uint16_t port = ntohs(client_info.sin_port);//把从网络中来的port从网络字节序转化成主机字节序
string ip = (inet_ntoa(client_info.sin_addr));//会自动把从网络中来的uint16_t整形的ip地址先从网络字节序转化成主机字节序,然后将主机字节序的整形ip转化string类型的ip
printf("[%s][%d]#:%s\n",ip.c_str(), port, c);
}
/*
走到这里开始处理从客户端读到
的信息,这需要经过一段的时间。
*/
//把从客户端读取到的信息处理完毕后,向客户端发出反馈信息
sendto(_sock, c, strlen(c), 0, (sockaddr*)&client_info, client_info_len);
}
}
private:
//一个服务器是需要ip和端口号的,不然其他机器找不到该服务器
string _ip;
uint16_t _port;//port表示端口,uint16_t是C语言中stdint.h头文件中定义的一种数据类型,它占据16个二进制位,范围从0到65535。它是无符号整数类型,即只能表示非负整数,没有符号位。
int _sock;//_sock是调用socket函数创建套接字时返回的值,本质就是文件描述符fd
};
正式开始网络测试
将上文中修改过的udp_server.cc和自始至终都没被修改过的udp_client.cc文件编译好后,直接启动服务端,注意根据我们编码的逻辑,在启动服务端进程时已经不用把服务端进程需要bind绑定的ip设置成命令行参数了,只在命令行参数中传入服务端进程所需要bind绑定的端口号port即可,如下图右半部分所示。
在<<套接字socket编程的基础知识点>>一文中说过,当前进程bind绑定INADDR_ANY地址后,不光可以让其他主机上的进程访问当前进程,是还可以让本地(即本机)上的其他进程访问当前进程的。所以下图右半部分的上面,我们测试了本地的客户端进程向服务端进程发送信息,可以发现测试成功。
那其他主机上的进程如何向本机的服务端进程发送信息呢?说一下,因为我们客户端的代码(即udp_client.cc文件)是使用的Linux的系统接口编写的,所以通过该文件编译出的可执行程序也只能在Linux机器上跑,所以你得先输入sz指令将编译好的客户端的可执行文件从云服务器上下载到本机(Windows系统),如下图所示,指令的格式为【sz+客户端的可执行文件的文件名】,然后将该文件发给你的小伙伴,当你的朋友收到这个客户端的可执行程序后,可以通过rz
命令或拖拽的方式将这个可执行程序上传到他的云服务器上,然后通过chmod
命令给该文件加上可执行权限。因为此时你的服务端进程已经启动了(即在上图中就已经启动了),所以你的朋友在命令行中输入指令【./udp_client+服务端进程所在的云服务器的虚拟ip地址+8080】即可连接成功,就可以正常通信了,这就是一个简易版本的网络服务器了。