目录
- socket 编程接口
- socket 常见的API
- sockaddr结构
- UDP网络程序简单例子
- 服务端代码编写
- 服务端创建套接字
- 服务端绑定
- 运行服务器
- 测试启动服务端
- 客户端代码编写
- 客户端创建套接字
- 启动客户端
- 本地测试
- INADDR_ANY
- 服务端接收信息发回到客户端
- 如何进行网络测试
socket 编程接口
socket 常见的API
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockaddr结构
sockaddr结构的出现
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。
为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_in和sockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。
此时当我们在传递在传参时,就不用传入sockeaddr_in或sockeaddr_un这样的结构体,而统一传入sockeaddr这样的结构体。在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信,在这些API内部就可以提取sockeaddr结构头部的16位进行识别,进而得出我们是要进行网络通信还是本地通信,然后执行对应的操作。此时我们就通过通用sockaddr结构,将套接字网络通信和本地通信的参数类型进行了统一。
注意: 实际我们在进行网络通信时,定义的还是sockaddr_in这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr*罢了。
UDP网络程序简单例子
我们要编写一个服务端和一个客户端。服务端能够接收客户端发过来的数据,数据处理以后再发给客户端。
服务端代码编写
服务端创建套接字
socket函数
创建套接字的函数叫做socket,该函数的函数原型如下:
int socket(int domain, int type, int protocol);
参数说明:
-
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表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
返回值说明: -
套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。
socket函数属于什么类型的接口?
网络协议栈是分层的,按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码,因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口。
socket函数是被谁调用的?
socket这个函数是被程序调用的,但并不是被程序在编码上直接调用的,而是程序编码形成的可执行程序运行起来变成进程,当这个进程被CPU调度执行到socket函数时,然后才会执行创建套接字的代码,也就是说socket函数是被进程所调用的。
socket函数底层做了什么?
socket函数是被进程所调用的,而每一个进程在系统层面上都有一个进程地址空间PCB(task_struct)、文件描述符表(files_struct)以及对应打开的各种文件。而文件描述符表里面包含了一个数组fd_array,其中数组中的0、1、2下标依次对应的就是标准输入、标准输出以及标准错误。
当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的“网络文件”,最后3号文件描述符作为socket函数的返回值返回给了用户。
其中每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read和write)在内核当中就是由struct file_operations结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。
对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。
服务端创建套接字
当我们在进行初始化服务器创建套接字时,就是调用socket函数创建套接字,创建套接字时我们需要填入的协议家族就是AF_INET,因为我们要进行的是网络通信,而我们需要的服务类型就是SOCK_DGRAM,因为我们现在编写的UDP服务器是面向数据报的,而第三个参数之间设置为0即可。
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
class UdpServer
{
public:
void Init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success : " << _sock << std::endl;
}
~UdpServer()
{
close(_sock);
}
private:
int _sock;//文件描述符
};
注意:当析构时,可以把对应的_sock对应的文件关闭,但是一般服务器不需要关闭。
测试创建套接字是否成功
#include "server.hpp"
using namespace std;
int main()
{
UdpServer* usvr = new UdpServer();
usvr->Init();
return 0;
}
运动代码,发现创建的socket文件描述符确实是3
服务端绑定
现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。
所以现在要将该文件和网络进行绑定。
bind函数
该函数原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
- sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
- addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:传入的addr结构体的长度。
返回值说明:
- 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。
#include <netinet/in.h>
struct sockaddr_in结构体
在前面我们讲过,如果需要进行网络通信,需要使用的是sockaddr_in结构体。
在绑定时需要把网络相关的属性填充进该结构体,然后作为bind函数的第二个参数。
下图是struct addrsock_in 的定义。
- 第一个参数表示的是协议家族
其中__SOCKADDR_COMMON 是被宏定义过的
“##” 的作用的是在宏定义的时候把形参和family拼到一起
242行代码相当于
sa_family_t sin_family
- sin_port 表示端口号,in_port_t 为uint16_t,一个无符号16位的整数
- sin_addr 表示IP地址
剩下的字段一般不做处理,当然你也可以进行初始化。
其中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的。
如何理解绑定?
在进行绑定的时候需要将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来。
增加IP地址和端口号
由于绑定时需要用到IP地址和端口号,因此我们需要在服务器类当中引入IP地址和端口号,在创建服务器对象时需要传入对应的IP地址和端口号,此时我们就可以根据传入的IP地址和端口号对对应的成员进行初始化。
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
class UdpServer
{
public:
UdpServer(std::string ip,in_port_t port)
:_ip(ip),_port(port)
{}
void Init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success : " << _sock << std::endl;
}
~UdpServer()
{
close(_sock);
}
private:
int _sock;
in_port_t _port;
std::string _ip;
};
服务端绑定
创建套接字完成后,就可以绑定。绑定之前需要先完成对 struct sockaddr_in 结构体的填充。由于该结构体中除了我们必须要填的协议家族、端口号、IP地址,还有部分选填字段,所以在填充之前,最好先清空一些该结构体变量的内容。
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <cstring>
class UdpServer
{
public:
UdpServer(std::string ip, in_port_t port)
: _ip(ip), _port(port)
{
}
void Init()
{
//创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success : " << _sock << std::endl;
//填充网络相关信息
struct sockaddr_in local;
bzero(&local, sizeof(local));//清空原先结构体里的内容
local.sin_family = AF_INET;//填写协议家族
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
//绑定
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定失败
{
std::cerr << "bind error" << std::endl;
exit(-2);
}
std::cout << "bind success" << std::endl;
}
~UdpServer()
{
close(_sock);
}
private:
int _sock;
in_port_t _port;
std::string _ip;
};
相关说明:
- 在发送到网络之前,需要将端口号设置为网络序列,htons函数提供了这样的功能。
- 在网络当中传输的是整数IP,所以我们需要将字符串形式的IP转化为整数IP,inet_addr提供了这样的功能。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);//字符串转整数
- 当网络属性信息填充完毕后,由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将struct sockaddr_in强转为struct sockaddr类型后再进行传入。
运行服务器
UDP服务器的初始化只需要创建和绑定就好了,初始化完成后,就可以运行服务器了。
服务器实际上就是在周而复始的为我们提供某种服务,服务器之所以称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是一个死循环代码。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。
recvfrom函数
UDP服务器读取数据的函数叫做recvfrom,该函数的函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
- sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
- buf:读取数据的存放位置。
- len:期望读取数据的字节数。
- flags:读取的方式。一般设置为0,表示阻塞读取。
- src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
返回值说明:
- 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
注意:
- 由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
- 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
- 由于recvfrom函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct sockaddr_in类型进行强转。
启动服务器函数
现在服务器通过recvfrom函数来读取客户端数据。在这之前,也需要创建一个struct sockaddr_in 结构体来保存客户端数据,再从该结构体里面获取客户端的端口号和IP地址。
我们将读取到的数据当做字符串看待,将读取到的数据最后一个位置设置为'\0'
,此时就可以将获取到的数据进行输出查看。
**注意:**服务器获取到的端口号是网络序列,需要转成主机序列。获取到的IP地址是整数IP,需要转化为字符串形式的IP
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <cstring>
class UdpServer
{
public:
UdpServer(std::string ip, in_port_t port)
: _ip(ip), _port(port)
{
}
void Init()
{
//创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success : " << _sock << std::endl;
//填充网络相关信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
//绑定
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定失败
{
std::cerr << "bind error" << std::endl;
exit(-2);
}
std::cout << "bind success" << std::endl;
}
void Start()//服务器启动
{
char buffer[1024];
while(true)//服务器死循环服务
{
struct sockaddr_in peer;//为接收客户端数据创建的结构体
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0';
in_port_t port = ntohs(peer.sin_port);//网络序列转主机序列
std::string ip = inet_ntoa(peer.sin_addr);//整数IP转字符串IP
std::cout << ip <<" : " << port << "#" << buffer << std::endl;
}
else
{
std::cerr << "recvfrom error" << std::endl;
}
}
}
~UdpServer()
{
close(_sock);
}
private:
int _sock;
in_port_t _port;
std::string _ip;
};
注意: 如果调用recvfrom函数读取数据失败,我们可以打印一条提示信息,但是不要让服务器退出,服务器不能因为读取某一个客户端的数据失败就退出。
测试启动服务端
服务端初始化和启动代码已经编写完了,接下来进行一下测试。
本地环回地址:127.0.0.1 主要是本地做测试使用的IP
我们讲IP地址设置为127.0.0.1,端口号设置为8081
#include "server.hpp"
using namespace std;
int main()
{
UdpServer* usvr = new UdpServer("127.0.0.1",8081);
usvr->Init();
usvr->Start();
return 0;
}
运行结果:
虽然现在客户端代码还没有编写,但是我们可以通过netstat命令来查看当前网络的状态,这里我们可以选择携带nlup选项。
netstat常用选项说明:
- -n:直接使用IP地址,而不通过域名服务器。
- -l:显示监控中的服务器的Socket。
- -t:显示TCP传输协议的连线状况。
- -u:显示UDP传输协议的连线状况。
- -p:显示正在使用Socket的程序识别码和程序名称。
此时被白色方框框住的那一行就是我们的UDP服务器的网络信息
其中netstat
命令显示的信息中,Proto
表示协议的类型,Recv-Q
表示网络接收队列,Send-Q
表示网络发送队列,Local Address
表示本地地址,Foreign Address
表示外部地址,State
表示当前的状态,PID
表示该进程的进程ID
,Program name
表示该进程的程序名称。
其中Foreign Address
写成0.0.0.0:*
表示任意IP地址、任意的端口号的程序都可以访问当前进程。
客户端代码编写
有服务端,就要有相应的客户端。
同样地,我们把客户端也封装成一个类。创建出一个客户端对象后也需要对齐初始化,客户端初始化的时候也需要像服务端那样创建套接字,之后客户端进行发送数据或者接收数据也就是对这个套接字进行操作。
客户端创建套接字
客户端创建套接字时选择的协议家族也是AF_INET,需要的服务类型也是SOCK_DGRAM,当客户端被析构时也可以选择关闭对应的套接字。与服务端不同的是,客户端在初始化时只需要创建套接字就行了,而不需要我们自己进行绑定操作。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
class UdpClient
{
public:
void Init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success" << std::endl;
}
~UdpClient()
{
close(_sock);
}
private:
int _sock;
};
客户端为什么不需要我们自己bind操作?那谁来绑定?
客户端不需要自己绑定端口号,交给操作系统去绑定。
原因主要是防止端口号冲突。
现在有这么一个场景:每个软件都需要有自己的端口号,如果自己绑定的话,软件的的端口号冲突了,就会造成软件启动冲突。比如软件A的端口号交给我们绑定的话是8081,软件B的端口号我们绑定也是8081,那么假如启动了软件A,那么可能会导致软件B无法正常启动,因为端口号被占用了。所以需要客户端的端口号需要交给操作系统去分配,绑定,避免冲突。
服务端端口号为什么需要自行绑定?
- 1.server的端口不能随意改变。
- 2.同一家公司的port号需要统一规范化
启动客户端
增加服务端IP地址和端口号
作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,因此在客户端类当中需要引入服务端的IP地址和端口号,此时我们就可以根据传入的服务端的IP地址和端口号对对应的成员进行初始化。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
class UdpClient
{
public:
UdpClient(std::string ip,in_port_t port)
:_server_ip(ip),_server_port(port)
{}
void Init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success" << std::endl;
}
~UdpClient()
{
close(_sock);
}
private:
int _sock;
in_port_t _server_port;
std::string _server_ip;
};
当客户端初始化完毕后我们就可以将客户端运行起来,由于客户端和服务端在功能上是相互补充的,既然服务器是在读取客户端发来的数据,那么客户端就应该想服务端发送数据。
sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
- sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
- buf:待写入数据的存放位置。
- len:期望写入数据的字节数。
- flags:写入的方式。一般设置为0,表示阻塞写入。
- dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。 addrlen:传入
- dest_addr结构体的长度。
返回值说明:
- 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
注意:
- 由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
- 由于sendto函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct sockaddr_in类型进行强转。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
class UdpClient
{
public:
UdpClient(std::string ip,in_port_t port)
:_server_ip(ip),_server_port(port)
{}
void Init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success" << std::endl;
}
void Start()
{
struct sockaddr_in peer;
bzero(&peer,sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = ntohs(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
while(true)
{
std::string msg;
std::cout << "Please Enter# ";
getline(std::cin,msg);
sendto(_sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
}
}
~UdpClient()
{
close(_sock);
}
private:
int _sock;
in_port_t _server_port;
std::string _server_ip;
};
本地测试
我们创建一个客户端对象,然后服务端的IP地址是本地环回地址,也就说“127.0.0.1”,服务端端口号为8081
#include "client.hpp"
using namespace std;
int main()
{
UdpClient* uclt = new UdpClient("127.0.0.1",8081);
uclt->Init();
uclt->Start();
}
启动服务端
启动客户端
客户端发送一条消息给服务端
服务端成功接收
此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8081,客户端的端口是54734。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。
INADDR_ANY
现在我们已经通过了本地测试,接下来就需要进行网络测试了,那是不是直接让服务端绑定我的公网IP,此时这个服务端就能够被外网访问了呢?
现在将服务端设置的IP改为我的公网IP,此时当我们重新编译程序再次运行服务端的时候会发现服务端绑定失败。
由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。
因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。
如果你既想让外网访问你的服务器,但你又指向绑定某一个IP地址,那么就不能用云服务器,此时可以选择使用虚拟机或者你自定义安装的Linux操作系统,那个IP地址就是支持你绑定的,而云服务器是不支持的。
更改代码
因此,如果想要让外网访问我们的服务,我们这里就需要将服务器类当中IP地址相关的代码去掉,而在填充网络相关信息设置struct sockaddr_in结构体时,将设置的IP地址改INADDR_ANY
就行了。由于INADDR_ANY的值本质就是0,不存在大小端的问题,因此在设置时可以不进行网络字节序的转换。
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <cstring>
class UdpServer
{
public:
UdpServer(in_port_t port = 8081)
: _port(port)
{
}
void Init()
{
//创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success : " << _sock << std::endl;
//填充网络相关信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
//绑定
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定失败
{
std::cerr << "bind error" << std::endl;
exit(-2);
}
std::cout << "bind success" << std::endl;
}
void Start()
{
char buffer[1024];
while(true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0';
in_port_t 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;
}
}
}
~UdpServer()
{
close(_sock);
}
private:
int _sock;
in_port_t _port;
//std::string _ip;//使用云服务器时去掉
};
此时当我们再重新编译运行服务器时就不会绑定失败了,并且此时当我们再用netstat命令查看时会发现,该服务器的本地IP地址变成了0.0.0.0,这就意味着该UDP服务器可以在本地读取任何一张网卡里面的数据。
服务端接收信息发回到客户端
我们这里写一个简单的服务:当服务端接收到客户端发来的信息后,服务端会发回消息给客户端。
当服务端收到客户端发来的数据后,除了在服务端进行打印以外,服务端可以调用sento函数将收到的数据重新发送给对应的客户端。
需要注意的是,服务端在调用sendto函数时需要传入客户端的网络属性信息,但服务端现在是知道客户端的网络属性信息的,因为服务端在此之前就已经通过recvfrom函数获取到了客户端的网络属性信息。
服务端代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <cstring>
class UdpServer
{
public:
UdpServer(in_port_t port = 8081)
: _port(port)
{
}
void Init()
{
//创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success : " << _sock << std::endl;
//填充网络相关信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); ;
//绑定
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定失败
{
std::cerr << "bind error" << std::endl;
exit(-2);
}
std::cout << "bind success" << std::endl;
}
void Start()
{
char buffer[1024];
while(true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
buffer[n] = '\0';
in_port_t 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;
}
std::string client_msg = "server get messag:";
client_msg += buffer;
sendto(_sock,client_msg.c_str(),client_msg.size(),0,(struct sockaddr*)&peer,len);
}
}
~UdpServer()
{
close(_sock);
}
private:
int _sock;
in_port_t _port;
//std::string _ip;//使用云服务器时去掉
};
服务端的代码改了之后,对应客户端的代码也得改改。当客户端发完数据给服务端后,由于服务端还会将该数据重新发给客户端,因此客户端发完数据后还需要调recvfrom来读取服务端发来的响应数据。
在客户端调用recvfrom函数接收服务端发来的响应数据时,客户端同时也需要读取服务端与网络相关的各种信息。虽然客户端早已知道服务端的网络信息了,此时服务端的网络信息已经不重要了,但还是建议不要把参数设置为空,这样可能会出问题,所以我们还是用一个临时变量将服务端的网络信息读取一下。
而客户端接收到服务端的响应数据后,将数据原封不动的打印出来就行了。此时客户端发送给服务端的数据,除了在服务端会打印显示以外,服务端还会将数据再重新发回给客户端,此时客户端也会接收到响应数据然后将该数据进行打印。
客户端代码
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
class UdpClient
{
public:
UdpClient(std::string ip,in_port_t port)
:_server_ip(ip),_server_port(port)
{}
void Init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
std::cout << "create socket success" << std::endl;
}
void Start()
{
struct sockaddr_in peer;
bzero(&peer,sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = ntohs(_server_port);
peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());
while(true)
{
std::string msg;
std::cout << "Please Enter# ";
std::getline(std::cin,msg);
sendto(_sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
char buffer[1024];
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t n = recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&tmp,&len);
if(n > 0)
{
buffer[n] = '\0';
std::cout << buffer << std::endl;
}
}
}
~UdpClient()
{
close(_sock);
}
private:
int _sock;
in_port_t _server_port;
std::string _server_ip;
};
运行服务端
#include "server.hpp"
using namespace std;
int main()
{
UdpServer* usvr = new UdpServer(8081);
usvr->Init();
usvr->Start();
return 0;
}
运行客户端
#include "client.hpp"
using namespace std;
int main()
{
UdpClient* uclt = new UdpClient("127.0.0.1",8081);
uclt->Init();
uclt->Start();
}
可以看到,当客户端发送消息给服务端时,服务端接收到数据后就把收到的消息发回来了。
实际上我们还可以让服务端对客户端发来的信息做各种处理,然后再发给客户端。这是只是简单举个例子。
如何进行网络测试
可以把这份客户端的代码文件发给别人,然后你在本地运行服务端。让别人运行客户端,输入你的公网IP地址和端口号,就可以进行测试了。
该文章参考自2021dragon