前言
了解了socket
编程的基础知识,IP + port标识网络中的唯一进程、源IP/端口号、目的IP/端口号以及socket
相关API
;
传输层的两大协议:TCP
/UDP
这里来使用UDP
相关接口来实现网络通信
服务端
1. 初始化
我们知道,在操作系统中并不是所有的进程都有端口号,所以在服务器端,就要做相关操作来让进程绑定唯一的端口号。
创建socket
对于服务器端,要先创建
socket
(IP
+port
)
创建socket
要用到接口socket
c
int socket(int domain, int type, int protocol);
参数:
socket
存在三个参数:int domain
、int type
、int protocol
domain
:

简单来说该参数就是表面要进行的通信类型,这里我们要进行网络通信,参数就传AF_INET
。
type
:

对于这个参数也是介绍了一大堆,这里就暂且先关注UPD
通信(也就是面向数据报 ),其中SOCK_DGRAM
就表示面向数据报。
所有,在传参时,
domain
传AF_INET
(表示网络通信)、type
传SOCK_DGRAM
(表示面向数据报)就能够表示当前是UDP
通信。

protocol
:
对于这个参数,这里暂时不做介绍,在使用时传递0
即可。
返回值:
对于
socket
的返回值,就有意思了:
可以看到,如果调用socket
创建成功,就会返回一个文件描述符 (这里体现了Linux
一切皆文件)而我们还知道,一个进程默认会打开三个文件(
0
、1
、2
文件描述符被占用),所以默认(不关闭0
、1
、2
)socket
返回的文件描述符是>=3
的。
这里暂且先不探究其原理,后续再深入了解。
所以,要使用UDP
通信,创建套接字,调用socket
就要传递AF_INET
、SOCK_DGRAM
、0
;
而socket
创建成功会返回一个文件描述符,这里就设计一个类,将该文件描述符管理起来。
cpp
//udpserver.hpp
class UdpServer
{
public:
UdpServer() : _sockfd(-1)
{
}
~UdpServer() {}
void Init()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信 SOCK_DGRAM 面向数据报
if (_sockfd < 0)
{
// _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出
LOG(Level::DEBUG) << "socket error";
exit(1);
}
LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;
}
private:
int _sockfd;
};
这样实现UdpSrever
,在服务器端只需创建UdpServer
对象,然后调用Init
(初始化)和运行成员函数即可。
cpp
//udpserver.cc
#include "udpserver.hpp"
int main()
{
UdpServer usvr;
usvr.Init();
return 0;
}

可以看到,socket
返回的文件描述符确实是从3
开始的。
绑定IP/端口号
在上述操作中,通过socket
创建了套接字,也获取了文件描述符;但是还没有IP
和端口号啊,还是没办法在网络中找到该主机的该进程啊;
所以创建完套接字之后,还要进行绑定IP和端口号;就要调用bind
方法
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
可以看到bind
应该是哪个参数,其第一个参数是sockfd
,就需要传递socket
返回的文件描述符。
addr
、addrlen
:
首先
const struct sockaddr* addr
,这里我们要进行网络通信,就要传递一个struct sockaddr_in *
的指针变量;而
socklen_t addrlen
则表示传递的第二个参数的长度。
所以,在调用bind
时,我们要进行网络通信就要先有一个struct sockaddr_in
类型的结构体对象;
- 构建
struct sockaddr_in
对象我们知道
sockaddr_in
结构体中存在三个字段:sin_family
、sin_addr
和sin_port
;分别指标志位、IP地址和端口号。这里要进行网络通信,标志位就传递
AF_INET
;这里在服务器端,就由外部指定要绑定的
IP
和端口号。(在运行程序时由命令行参数传递)
IP
地址转换在我们的认知中,
IP
地址都是10.0.16.12
这种形式的,但是在这里struct sockaddr_in
结构中的IP地址类型是uint32_t
,也就是4字节整数;那我们通过命令行参数获取到的IP地址是字符串形式的(.分隔开的数字),所以在这里就要进行IP地址的转换(由字符串形式转为
4
字节数字)此外,我们这里的
sockaddr_in
在未来进行通信时,是要发送到网络的,所以在这里我们还需要将本地字节序转换为网络字节序这里我们可以自己实现字符串IP --> 4字节数字,本地字节序 --> 网络字节序。但是这里,我们也可以调用
inet_addr
(将字符串形式的IP地址转换为4字节数字,再转换为网络字节序)
- 端口号转换
这里的
struct sockaddr_in
是要发送到网络的,所以端口号sin_port
也要由本机字节序转换为网络字节序,这里就可以调用htons
来进行转换。
返回值:

成功,0
被返回;失败则返回-1
,并且错误码被设置。
cpp
class UdpServer
{
public:
UdpServer(const std::string &ip, uint16_t port) : _sockfd(-1), _ip(ip), _port(port)
{
}
~UdpServer() {}
void Init()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信 SOCK_DGRAM 面向数据报
if (_sockfd < 0)
{
// _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出
LOG(Level::DEBUG) << "socket error";
exit(1);
}
LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;
// 2.1 构建sockaddr_in对象
struct sockaddr_in sockin;
sockin.sin_family = AF_INET;
sockin.sin_addr.s_addr = inet_addr(_ip.c_str());
sockin.sin_port = htons(_port);
// 2.2 绑定IP、端口号
int n = bind(_sockfd, (struct sockaddr *)&sockin, sizeof(sockin));
if (n < 0)
{
// n < 0 表示绑定失败,这里输出一条日志然后退出
LOG(Level::DEBUG) << "bind error";
exit(2);
}
LOG(Level::DEBUG) << "socket success";
}
private:
int _sockfd;
std::string _ip;
uint16_t _port;
};
这样,服务器在创建UpdServer
对象,调用Init
初始化后才能进行通信。
cpp
//udpserver.cc
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << argv[0] << " ip port" << std::endl;
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
UdpServer usvr(ip, port);
usvr.Init();
while (true)
{
}
return 0;
}
这样在程序运行起来之后,就可以在系统中查看到当前进程绑定的IP和端口号了
netstat可以用来查看网络相关信息
-u
表示UDP
相关;-p
表示查看进程pid
和program name
-n
表示用数字显示-l
表示查看监听状态的端口

2. 接收/发送信息
通过创建套接字、绑定IP和端口号,当前就已经具备了通信的条件,现在来实现接受和发送信息;
接受信息
对于UDP
通信,接受信息要用到的接口就是recv
系列的接口,这里使用recvfrom
c
ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
int flags,
struct sockaddr *_Nullable restrict src_addr,
socklen_t *_Nullable restrict addrlen);
参数:
recvfrom
存在6
个参数:
sockfd
:socket
返回的文件描述符,指明通信使用的套接字(文件)buf
、len
:buf
表示接受信息的缓冲区,len
表示缓冲区大小flags
:标志为,这里传0
即可**(表示阻塞式接受信息)**src_addr
、addrlen
:recvfrom
不仅受到了对方发来的信息,还收到对方的struct sockaddr
字段(IP
、port
);
src_addr
输出型参数,recvfrom
接收到对方的struct sockaddr
字段拷贝到该地址;
addrlen
传参时表示src_addr
的长度,(也是输出型参数,调用完成后,addrlen
中的值表示实际读到的长度)
返回值:
recvfrom
读取成功,返回实际读到信息的字节数;(buf
)
发送信息
要发送信息,这里就要使用send
系列接口,这里使用sendto
c
ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
参数和
recvfrom
一样:sockfd
指创建socket
时返回的文件描述符、buf
表示要发送的信息,len
表示信息的长度;
flag
这里暂时传递0
即可。
dest_addr
表示要发送给谁,目的主机的struct sockaddr
字段;addrlen
表示dest_addr
的长度(大小)。
返回值这里暂时不考虑。
所以,我们就可以让服务器端阻塞等待式的接受信息,收到信息之后,对信息稍作处理,然后再发送回来。
cpp
class UdpServer
{
public:
void Start()
{
while (true)
{
char buff[256];
struct sockaddr_in peer;
socklen_t len;
// 接受信息
int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);
if (n < 0)
{
// 读取失败
LOG(Level::WARNING) << "recvfrom error";
continue;
}
buff[n] = '\0';
std::cout << "recv massage :" << buff << std::endl;
// 发送信息
int m = sendto(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, len);
if (m < 0)
{
// 读取失败
LOG(Level::WARNING) << "sendto error";
continue;
}
}
}
private:
int _sockfd;
std::string _ip;
uint16_t _port;
};
这里,服务器端在接受到远端发送的信息,输出一条消息到标准输出,然后再信息发送给远端。
客户端
上面实现了服务端的代码,现在来实现客户端。
对于服务端,在通信之前,要先创建套接字,绑定IP和端口号;
那客户端呢?
客户端,只需要我们显示地去创建套接字即可,不需要显示绑定(在首次发送信息时会自动绑定IP地址和随机端口号)
所以,客户端只需要创建套接字,然后就可以给服务端发送信息了。
而发送信息,要知道给谁发吧,那就需要对方的IP地址和端口号;
这里就通过命令行参数,在执行程序时指定IP和端口号。
cpp
//udpclient.cc
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
int main(int agrc, char *argv[])
{
if (agrc != 3)
{
std::cout << argv[0] << " serverip serverport" << std::endl;
return -1;
}
// 创建服务端 struct sockaddr_in
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(std::stoi(argv[2]));
// 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return -1;
}
// 无需显示绑定
while (true)
{
std::string massage;
std::getline(std::cin, massage);
// 发送信息
sendto(sockfd, massage.c_str(), massage.size(), 0, (struct sockaddr *)&server, sizeof(server));
std::cout << "send massage : " << massage << std::endl;
// 接受信息
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(len);
char buff[256];
int n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);
if (n < 0)
{
// 读取失败
std::cerr << "recvfrom error";
continue;
}
buff[n] = '\0';
std::cout << "recv : " << buff << std::endl;
}
return 0;
}
这里client
和server
都只是简单的接受和发送数据,并没有进行数据处理。

本机IP
在上述操作中,使用的10.0.16.12
IP地址是云服务器的子网IP,我们如果尝试去连接公网IP,可以发现是连不上的(服务器公司做相关保护)
而在服务器中还存一种IP地址,就是本机IP:
使用ifconfig
命令可以查看到:

就是上图中的127.0.0.1
,如果我们一台机器上的server
和client
通信使用这种IP地址,数据就不会传输到网络,而是通过操作系统发送到对方。
这种IP 地址也通常用来测试网络代码。

可以看到也是可以通信的。
但是,如果这里一端使用10.0.16.12
地址,另外一端使用127.0.0.1
呢?


可以看到,client和server绑定不同的IP地址,虽然是一台主机的IP地址,但却无法完成通信。
而发送到127.0.0.1
和10.0.16.12
的信息都是发送给该主机的啊,按理来说应该是能够收到的。
这里,要想让服务端接收到发送给该主机的所有信息,就不能让server去绑定某个IP地址,而是让server绑定INADDR_ANY
;

INADDR_ANY
本质上就是0
。
所以,在server
端,就不需要通过命令行参数传递IP
地址,也不需要存储IP地址了,直接绑定INADDR_ANY
即可。
cpp
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port) : _sockfd(-1), _ip(ip), _port(port)
// {
// }
UdpServer(uint16_t port) : _sockfd(-1), _port(port)
{
}
~UdpServer() {}
void Init()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信 SOCK_DGRAM 面向数据报
if (_sockfd < 0)
{
// _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出
LOG(Level::DEBUG) << "socket error";
exit(1);
}
LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;
// 2.1 构建sockaddr_in对象
struct sockaddr_in sockin;
bzero(&sockin, sizeof(sockin));
sockin.sin_family = AF_INET;
// sockin.sin_addr.s_addr = inet_addr(_ip.c_str());
sockin.sin_addr.s_addr = INADDR_ANY;//绑定INADDR_ANY 接受发送给该主机的所有信息
sockin.sin_port = htons(_port);
// 2.2 绑定IP、端口号
int n = bind(_sockfd, (struct sockaddr *)&sockin, sizeof(sockin));
if (n < 0)
{
// n < 0 表示绑定失败,这里输出一条日志然后退出
LOG(Level::DEBUG) << "bind error";
exit(2);
}
LOG(Level::DEBUG) << "socket success";
}
void Start()
{
while (true)
{
char buff[256];
struct sockaddr_in peer;
socklen_t len;
// 接受信息
int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);
if (n < 0)
{
// 读取失败
LOG(Level::WARNING) << "recvfrom error";
continue;
}
buff[n] = '\0';
std::cout << "recv massage :" << buff << std::endl;
// 发送信息
int m = sendto(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, len);
if (m < 0)
{
// 读取失败
LOG(Level::WARNING) << "sendto error";
continue;
}
}
}
private:
int _sockfd;
// std::string _ip;
uint16_t _port;
};
到这里本篇文章内容就结束了,感谢各位大佬的支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws