我们把服务封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务区需要做的第一件事就是创建套接字。
返回值说明:
socket属于什么接口?
网络协议栈是分层的,根据TCP/IP四层协议,自顶向下以此是应用层、传输层、网络层、数据链路层。而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码,因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口。
socker函数在底层做了什么?
socket函数是被进程所调用的,而每一个进程在系统层面上都有一个管理进程的PCB、文件描述符表(files_struct)以及对于打开的文件。而文件描述符表内包含了一个数组fd_array,其中数组的0、1、2下标默认被标准输入、标准输出以及标准错误所占用。
当我们在进行初始化服务器创建套接字时,就是调用socket函数创建套接字,创建套接字我们要传入的协议家族就是AF_INET,表明我们要进行的是网络通信,而我们需要的服务类型是SOCK_DGRAM,因为我们现在编写的UDP服务器是面向数据报的,而第三个参数设置为0即可。
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
class UdpServer
{
public:
bool InitServer()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
return true;
}
~UdpServer()
{
if (_sockfd >= 0)
close(_sockfd);
}
private:
int _sockfd; // 文件描述符
};
上面那段代码在hpp头文件中,下面我们在源文件中包含它,并运行起来,看看结果。
#include "UdpServer.hpp"
int main()
{
UdpServer* svr = new UdpServer();
svr->InitServer();
return 0;
}
运行结果如下:
现在套接字已经创建成功了,但只是在系统层面上打开了一个文件,操作系统并不知道是要将数据写入到磁盘还是网卡,此时该文件还没有与网络关联起来。
由于现在编写的是不面向连接的UDP服务器,所以初始化的第二件事就是绑定。
bind函数
服务端绑定用到的是bind函数
返回值说明:
struct sockaddr_in结构体
在绑定时我们需要将网络相关的属性信息填充到一个结构体当中,然后将该结构体作为bind函数的第二个参数进行传入,这实际就是struct sockaddr_in结构体。
struct sockaddr_in当中的成员如下:
剩下的字段一般不作处理。
如何理解绑定?
class UdpServer
{
public:
UdpServer(std::string ip, int port)
: _sockfd(-1), _port(port), _ip(ip)
{};
bool InitServer()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
return true;
}
~UdpServer()
{
if (_sockfd >= 0)
close(_sockfd);
}
private:
int _sockfd; // 文件描述符
int _port; // 端口号
std::string _ip; // IP地址
};
服务端绑定
需要注意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons函数将端口号转为网络序列。此外,由于网络中传输的是整数IP,我们需要调用inet_addr函数将字符串转换成整数IP,然后再将转换后的整数IP进行设置。(inet_addr函数同时做了将字符串转换为整数,以及将主机字节序转换为网络字节序两件事)。
bool InitServer()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << _sockfd << std::endl;
// 填充网络相关信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 绑定
int ret = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if (ret < 0)
{
std::cerr << "bind error" << std::endl;
return false;
}
std::cout << "bind success" << std::endl;
return true;
}
整数IP存在的意义
inet_addr函数
在进行字符串IP和网络IP的转换时,系统为我们提供了相应的转换函数,我们直接调用即可。
inet_ntoa函数
将整数IP转换为字符串IP函数为inet_ntoa
UDP服务器的初始化只需要创建套接字和绑定,当服务器初始化完毕之后就可以启动服务了。
服务器实际上就是在周而复始地为我们提供某种服务,服务器在运行起来之后就是一个死循环。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。
recvfrom
UDP服务器读取数据的函数叫做recvfrom
参数说明:
返回值说明:
注意:
代码实现
void start()
{
#define SIZE 1024
char buffer[SIZE];
while (1)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if (size > 0)
{
buffer[size] = 0;
int port = ntohs(peer.sin_port);
std::string ip = inet_ntoa(peer.sin_addr);
std::cout << ip << " : " << port << "# " << buffer << std::endl;
}
else
{
std::cerr << "recvfrom error" << std::endl;
}
}
}
注意:如果调用recvform函数读取数据失败,我们可以打印一条提示信息,但是不要让服务器退出,服务器不能因为读取某一个客户端的数据失败就退出。
引入命令行参数
int main(int argc, char* argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
std::string ip = "127.0.0.1"; // 本地环回
int port = atoi(argv[1]);
UdpServer* svr = new UdpServer(ip, port);
svr->InitServer();
svr->start();
return 0;
}
此时运行程序,就可以看到套接字创建成功、绑定成功了,现在服务器就是在等待客户端发送数据。
我们可以通过netstat命令查看当前网络的状态
netstat命令常用选项说明:
同样地,我们把客户端也封装成一个类,当我们定义出一个客户端对象后也是需要对其进行初始化,而客户端在初始化是也需要创建套接字,之后客户端发送数或接收数据也就是对这个套接字进行操作。
客户端创建套接字时选择的协议家族也是AF_INET,需要的服务类型也是SOCK_DGRAM,当客户端被析构时也可以选择对应的套接字。与服务端不同的是,客户端在初始化时只需要创建套接字就行了,不需要程序员进行绑定。
class UdpClient
{
public:
bool InitClient()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return false;
}
return true;
}
~UdpClient()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd;
}
客户端要不要进行bind?
client必须要进行bind,要有自己的IP和port,说不要bind是错的,只不过client不需要程序员去进行bind。服务端的port为了客户端更快地找到,它是固定不变的,但是客户端有很多,如果都要让客户处去显式地绑定一个port,程序员可能选的port是一样的。为了避免重复,port由操作系统自动生成并隐式地进行绑定。
所以,在UDP通信中写客户端时,只需要创建套接字即可,不需要bind,bind由操作系统自己完成!
class UdpClient
{
public:
UdpClient(std::string server_ip, int server_port)
: sockfd(-1), _server_ip(server_ip), _server_port(server_port)
{}
bool InitClient()
{
// 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return false;
}
return true;
}
~UdpClient()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd;
int _server_port; // 服务端端口号
std::string _server_ip // 服务端IP地址
}
sendto函数
UDP客户端发送数据的函数叫做sendto
参数说明:
返回值说明:
注意:
启动客户端函数
现在客户端要发送数据给服务端,我们可以让客户端获取用户输入,不断将用户输入的数据发送给服务端。
void start()
{
std::string msg;
struct sockaddr_int peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
while (1)
{
std::cout << "Please Enter# ";
getline(std::cin, msg);
sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
}
}
引入命令行参数
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
int server_port = atoi(argv[2]);
UdpClient* clt = new UdpClient(server_ip, server_port);
clt->InitClient();
clt->start();
return 0;
}
绑定INADDR_ANY的好处
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- huatuoyibo.net 版权所有 湘ICP备2023021910号-2
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务