【Linux网络】Socket编程UDP


1.Server端
1.1 初始化
1.1.1 创建套接字
首先我们要创建套接字,调用函数socket,成功返回一个文件描述符,失败返回-1.

- 第一个参数就是要做什么通信,有很多选项,进行网络通信就传AF_INET;

- 第二个参数要创建的套接字类型,也有很多,传SOCK_DGRAM

- 第三个参数是要设定的协议类型,其实这里AF_INET和SOCK_DGRAM就已经能说明是UDP协议了,这个参数设为0。
//UdpServer.hpp文件
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "MyLog.hpp"using namespace MyLog;
class UdpServer
{
public:UdpServer(): _sockfd(-1){}void Init(){// 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){LOG(LogLevel::FATAL) << "创建套接字失败";exit(1);}LOG(LogLevel::INFO) << "creat socket success, sockfd: " << _sockfd;}~UdpServer() {}private:int _sockfd;
};
创建套接字只是相当于打开了网络文件,将来要收发消息时,别人会知道我的ip和端口号吗?所以我们还要对socket进行绑定。
1.1.2 绑定socket信息
主要就是绑定ip和端口,需要用到的函数是bind。成功返回0,失败返回-1.

第一个参数就是前面创建的sockfd,第二个参数是一个结构体,第三个参数是这个结构体的大小
sockaddr结构:

要进行网络通信就传sockaddr_in, 要进行本地通信就传sockaddr_un。

typedef unsigned short int sa_family_t; //就是一个无符号短整数 //in_port_t其实就是uint16_t
/* Type to represent a port. */
typedef uint16_t in_port_t;//in_addr里面就是一个in_addr_t的变量,in_addr_t就是uint32_t
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr{in_addr_t s_addr;}; 所以其实这个sockaddr_in里就是有三个整数而已。
回到代码,在填写sockaddr_in结构体的信息之前要清0,这里用到的函数是bzero。
void Init(){// 1.创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){LOG(LogLevel::FATAL) << "创建套接字失败";exit(1);}LOG(LogLevel::INFO) << "creat socket success, sockfd: " << _sockfd;//绑定socket信息,IP和端口struct sockaddr_in local;bzero(&local, sizeof(local)); //先清0local.sin_family = AF_INET;local.sin_port = // ?local.sin_addr.s_addr = //?} 将来我们发数据的时候,还要把端口号也要发给别人,这就说明ip和端口信息也要发送到网络,所以我们需要将本地格式转网络序列。
- h表示host,n表示network,l表示32位长整数,s表示16位短整数,例如 htonl 表⽰将 32 位的⻓整数从主机字节序转换为⽹络字节序
local.sin_port = htons(_port); private:uint16_t _port; // 端口号
ip地址也是如此,前面说过IP是uint32_t 类型的,是4字节风格的IP地址,而服务器里保存的IP地址是字符串风格的地址,点分十进制,所以我们需要将ip转为4字节,还要将4字节转为网络序列,这两个步骤不需要我们自己完成,有一个函数叫inet_addr,可以完成这两个步骤。

而且这个函数的返回值类型就是in_addr_t,也就是前面说过的ip地址结构体里的变量的类型。
//UdpServer.hpp文件
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "MyLog.hpp"using namespace MyLog;
class UdpServer
{
public:UdpServer(std::string &ip, uint16_t &port): _sockfd(-1),_port(port),_ip(ip){}void Init(){// 1.创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){LOG(LogLevel::FATAL) << "创建套接字失败";exit(1);}LOG(LogLevel::INFO) << "creat socket success, sockfd: " << _sockfd;// 2.绑定socket信息,IP和端口struct sockaddr_in local;bzero(&local, sizeof(local)); // 先清0local.sin_family = AF_INET;local.sin_port = htons(_port); // 主机序列转网络序列// ip转4字节,4字节转网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str());}~UdpServer() {}private:int _sockfd;uint16_t _port; // 端口号std::string _ip;
};
sockaddr结构填写完成后,就可以绑定了,绑定时要对sockaddr_in结构进行强转。
void Init(){// 1.创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){LOG(LogLevel::FATAL) << "创建套接字失败";exit(1);}LOG(LogLevel::INFO) << "creat socket success, sockfd: " << _sockfd;// 2.绑定socket信息,IP和端口// 2.1 填写sockaddr_in结构体struct sockaddr_in local;bzero(&local, sizeof(local)); // 先清0local.sin_family = AF_INET;local.sin_port = htons(_port); // 主机序列转网络序列// ip转4字节,4字节转网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str());// 2.2 绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind失败";exit(2);}LOG(LogLevel::INFO) << "bind success";}
1.2 启动
1.2.1 收消息
一般软件运行起来后就是死循环。
public:void Start(){if (_isrunning)return; // 不要重复启动_isrunning = true;while(true){}}
private:int _sockfd;uint16_t _port; // 端口号std::string _ip;bool _isrunning;
服务器要先接受数据,再发送消息。
接收消息要用到函数recvfrom,成功返回收到的字节数,失败返回-1。
第一个参数就是套接字;第二个参数是接收数据的缓冲区;第三个参数是这个缓冲区的大小;第四个参数是阻塞IO或者非阻塞IO,阻塞式IO就是没有收到数据的时候进程会一直等,就等同于scanf,这里设置成0,表示默认非阻塞;第四个参数用于存储发送数据的对端的ip和端口号(即记录寄件人地址);第四个参数是src_addr的实际大小。
void Start(){if (_isrunning)return; // 不要重复启动_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer); // 大小要是socklen_t类型// 1.收消息size_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0; // 手动添加字符串结束标志LOG(LogLevel::DEBUG) << "buffer:" << buffer;}}}
1.2.2 发消息
发消息用到的接口是sendto。

参数和收消息接口类似:第一个参数就是套接字;第二个参数是发消息的缓冲区;第三个参数是这个缓冲区的大小;第四个参数是阻塞IO或者非阻塞IO,这里设置成0,表示默认非阻塞;第四个参数用于存储发送数据的对端的ip和端口号(即给谁发);第四个参数是src_addr的实际大小。
void Start(){if (_isrunning)return; // 不要重复启动_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer); // 大小要是socklen_t类型// 1.收消息size_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0; // 手动添加字符串结束标志LOG(LogLevel::DEBUG) << "buffer:" << buffer;// 2.发消息std::string echo_string = "server echo# ";echo_string += buffer;sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);}}}
1.3 源文件
//UdpServer.cc文件
#include "UdpServer.hpp"
#include <memory>// 格式为: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::stoi(argv[2]);Refresh_Log_To_Console();std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(ip, port);server->Init();server->Start();return 0;
}
2.Client端
Client端就不做封装了,直接在.cc文件里写。
客户端要访问服务器,在日常生活中网络请求都是由客户端发起,服务器处理。客户端必须知道目标服务器的套接字信息(ip+port),所以客户端启动的时候要传服务器的ip和port,格式应该为udpclient server_ip server_port。
但是客户端怎么知道服务器的ip和端口号呢?客户端和服务器是一家公司写的,客户端内部以一定形式内置了服务器的ip和端口号。
// UdpClient.cc文件
#include <iostream>
#include <sys/types.h>
#include <sys/socket.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 ip = argv[1];uint16_t port = std::stoi(argv[2]);return 0;
}
客户端也要创建套接字,调用函数socket,成功返回一个文件描述符,失败返回-1。
// 格式为: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 ip = argv[1];uint16_t port = std::stoi(argv[2]);// 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cout << "client创建套接字失败" << std::endl;return 2;}return 0;
}
客户端要不要绑定socket信息呢?要绑定。要不要显示的绑定呢?不要。
因为客户端首次发送消息,OS会自动给client进行bind,OS知道ip,而端口号采用随机端口号的方式,因为一个端口号只能被一个进程bind,采用随机端口号是为了避免client端口号冲突。
所以,客户端的端口号是多少并不重要,只要是唯一的就行;服务器端需要显示的bind,因为服务端会被很多个客户端访问,服务端的IP和端口必须是众所周知并且不能轻易改变的,一旦容易改变,客户端可能就访问不到了,就像生活中110或120这样的特殊电话号码是不能轻易改变的,但我们自己使用的手机号码可以任意换。
客户端要先发送消息,再接收消息。我们往服务器发消息,需要填写服务器信息。
// UdpClient.cc文件
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.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];uint16_t server_port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cout << "client创建套接字失败" << std::endl;return 2;}// 2.bind,但不需要显示的bind// 填写服务器信息struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 先请0server.sin_family = AF_INET;server.sin_port = htons(server_port); // 主机转网络server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // ip转4字节,4字节转网络序列while (true){// 3.发消息std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input); // 从输入流获取到input里sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));}return 0;
}
客户端收消息的时候也要知道是哪个服务器发的,因为一个客户端可能访问多个服务器。
while (true){// 3.发消息std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input); // 从输入流获取到input里sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));// 4.收消息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;}}
3.绑定IP问题
我们用ifconfig命令查询,会有如下显示。

172.17.55.42是这台机器的内网ip,暂时不管,而这个127.0.0.1,是本地环回。
本地环回:要求客户端和服务器必须在一台机器上,表明我们是本地通信,客户端发送数据不会推送到网络,而是在OS内部转一圈直接交给对应的服务器端。
这种ip地址经常用来进行网络代码的测试,因为如果把代码直接扔到网络里,有可能是因为网不好导致结果不对,所以我们先用这个ip先对代码进行测试。
#Makefile文件
.PHONY:all
all:udpclient udpserverudpclient:UdpClient.ccg++ -o $@ $^ -std=c++17
udpserver:UdpServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f udpclient udpserver
编译之后运行两份代码。

然后输入一些内容。

绑定内网IP也可以。

但是不可以直接绑定公网ip,因为公网IP并没有配置到我们的IP上。

并且如果服务器绑定内网IP,而客户端拿着本地环回ip访问,数据就会发不出去。

反过来也不行。
- netstat -anup:查网络连接状态,a表示所有,n表示数字化,u表示查看udp协议,p表示把进程信息也显示出来

客户端想访问ip为127.0.0.1 端口号为8080的地址信息,根本就不存在。
所以如果我们显式的bind,client未来访问的时候,就必须使用server端bind的地址信息。
公网IP不让邦,显式bind的话client又不好访问,所以服务器端不建议显式的bind,那怎么办呢?解决方法如下,把地址改为一个宏。
//UdpServer.hpp文件UdpServer类里的Init函数内部//local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY;
这个宏如下,其实就是0,设置成0后就可以ip任意bind,允许接收任何的信息。

所以,server端就不需要传ip,只需要传个端口号就行。
// UdpServer.hpp文件
class UdpServer
{
public:UdpServer(uint16_t &port) // 不需要传ip: _sockfd(-1),_port(port),_isrunning(false){}//...private:int _sockfd;uint16_t _port; // 端口号//std::string _ip; // 不需要ipbool _isrunning;
};
// UdpServer.cc文件
#include "UdpServer.hpp"
#include <memory>// 格式为:udpserver port
int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage:" << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[2]);Refresh_Log_To_Console();std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(port);server->Init();server->Start();return 0;
}
然后server端只需要绑定端口号,我们再用netstat查询一下网络连接状态。

现在就是显示的0.0.0.0:8080,就意味着用这台机器上的任意ip来访问server,他都可以向客户端发消息了。

前面是绑的本地环回,下面是绑的内网IP,都能给服务器发消息了。

但是公网IP不可以。

server获取client的IP和port
通过上面的实验我们会发现在server端我们并不知道消息是谁发送的,所以需要获取一下client的IP和port。client的信息就存储在recvfrom函数的第五个参数里,就是peer。
- 对于端口号,因为此是端口号是从网络里拿的,所以是网络序列,我们需要将网络序列转主机序列。
- 对于ip,我们需要的是点分十进制的字符串风格的IP,而从网络里拿到的是4字节网络风格的IP,所以需要用函数inet_ntoa进行转换。
// UdpServer.hpp文件void Start(){if (_isrunning)return; // 不要重复启动_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer); // 大小要是socklen_t类型// 1.收消息size_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0; // 手动添加字符串结束标志uint16_t peer_port = ntohs(peer.sin_port); // 网络序列转主机序列std::string peer_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer;// 2.发消息std::string echo_string = "server echo# ";echo_string += buffer;sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);}}}


4.服务器回调函数
收到消息后,之前我们是把这个buffer消息打印一下,现在我们需要设计一个服务器的回调函数,让这个回调函数来对数据进行处理,处理完成后把结果返回来就行。有了回调函数,在初始化的时候就要要求传对应的回调方法。
// UdpServer.hpp文件
#include <functional>
using namespace MyLog;
using func_t = std::function<std::string(const std::string&)>; // 参数和返回值都为string类型
class UdpServer
{
public:UdpServer(uint16_t &port, func_t func) // 初始化时要传回调方法: _sockfd(-1),_port(port),_isrunning(false),_func(func){}void Start(){if (_isrunning)return; // 不要重复启动_isrunning = true;while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer); // 大小要是socklen_t类型// 1.收消息size_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0; // 手动添加字符串结束标志uint16_t peer_port = ntohs(peer.sin_port); // 网络序列转主机序列std::string peer_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列 std::string result = _func(buffer); // 回调函数处理,并返回结果// 2.把结果发出去 sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);}}}private:int _sockfd;uint16_t _port; // 端口号bool _isrunning;func_t _func; // 服务器的回调函数
};
下面有个回调方法的例子。
// UdpServer.cc文件
#include "UdpServer.hpp"
#include <memory>std::string DefaultHandler(const std::string &messages)
{std::string s = "***";s = s + messages + "***";return s;
}int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage:" << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Refresh_Log_To_Console();// 初始化时传回调方法std::unique_ptr<UdpServer> server = std::make_unique<UdpServer>(port, DefaultHandler);server->Init();server->Start();return 0;
}

现在UdpServer只用来进行网络通信的,而怎么处理由上层决定。这就是简单的代码的层状结构。
本篇分享就到这里,我们下篇见~

