> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:网络
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
一、前言
前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!
二、主体
学习【网络】套接字编程——UDP通信咱们按照下面的图解:
2.1 概述
在使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质的差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。
以下给出了典型的UDP客户/服务器的函数调用。客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。类似的,服务器不接受来自客户端的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将接受与所接受的数据报一道返回客户的协议地址,因此服务器可以把响应的发送给正确的客户。
2.2 Udp Server(服务端)
接下来接下来实现一批基于 UDP
协议的网络程序,本节只介绍基于IPv4的socket网络编程。
2.2.1 核心功能
说明:
分别实现客户端与服务器,客户端向服务器发送消息,服务器收到消息后,回响给客户端,有点类似于 echo
指令,该程序的核心在于 使用 socket
套接字接口,以 UDP
协议的方式实现简单网络通信。
2.2.2 核心结构
程序由server.hpp server.cc client.hpp client.cc 组成,大体框架如下:
创建 server.hpp
服务器头文件:
#pragma once
#include <iostream>
namespace nt_server
{
class UdpServer
{
public:
// 构造
UdpServer()
{}
// 析构
~UdpServer()
{}
// 初始化服务器
void InitServer()
{}
// 启动服务器
void StartServer()
{}
private:
// 字段
};
}
创建 server.cc
服务器源文件:
#include <memory> // 智能指针相关头文件
#include "server.hpp"
using namespace std;
using namespace nt_server;
int main()
{
unique_ptr<UdpServer> usvr(new UdpServer());//使用智能指针创建了一个UdeServer对象
// 初始化服务器
usvr->InitServer();
// 启动服务器
usvr->StartServer();
return 0;
}
创建 client.hpp
客户端头文件:
#pragma once
#include <iostream>
namespace nt_client
{
class UdpClient
{
public:
// 构造
UdpClient()
{}
// 析构
~UdpClient()
{}
// 初始化客户端
void InitClient()
{}
// 启动客户端
void StartClient()
{}
private:
// 字段
};
}
创建 client.cc
客户端源文件:
#include <memory>
#include "client.hpp"
using namespace std;
using namespace nt_client;
int main()
{
unique_ptr<UdpClient> usvr(new UdpClient());
// 初始化客户端
usvr->InitClient();
// 启动客户端
usvr->StartClient();
return 0;
}
创建 Makefile
文件:
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server client
2.2.3 Udp Server 端代码
2.2.3.1 socket - 创建套接字
语法:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
解释说明:
- domain:协议域(协议族)。该参数决定了 socket 的地址类型。在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 地址(32位的)与端口号(16位)的组合,AF_UNIX 决定了要用一个绝对路径名作为地址,AF_INET6(IPv6)。
- type:指定了 socket 的类型,如 SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)等等。
- protocol:指定协议,如 IPPROTO_TCP(TCP传输协议)、PPTOTO_UDP(UDP 传输协议)、IPPROTO_SCTP(STCP 传输协议)、IPPROTO_TIPC(TIPC 传输协议)。
- 返回值:一个文件描述符,创建套接字的本质其实就是打开一个文件。
功能说明:
- socket函数打开一个网络通讯端口,如果成功的话就像open一样返回一个文件描述符,应用程序可以像读写文件一样read/write在网络上收发数据。
- 好了socket函数学完了,接下来在 server.hpp 的 InitServer() 函数中创建套接字,并对创建成功/失败后的结果做打印。
代码呈现:
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
namespace nt_server
{
// 错误码
enum
{
SOCKET_ERR = 1
};
class UdpServer
{
public:
// 构造
UdpServer()
{}
// 析构
~UdpServer()
{}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_ == -1)//创建失败
{
std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 创建成功
std::cout << "Create Success Socket: " << sock_ << std::endl;
}
// 启动服务器
void StartServer()
{}
private:
int sock_; // 套接字
};
}
总结说明:
因为这里是使用 UDP 协议实现的 网络通信,参数1 domain 选择 AF_INET(基于 IPv4 标准),参数2 type 选择 SOCK_DGRAM(数据报传输),参数3设置为 0,可以根据 SOCK_DGRAM 自动推导出使用 UDP 协议。
2.2.3.2 bind - 将套接字与一个 IP 和端口号进行绑定
语法:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
解释说明:
- sockfd: 通过socket函数得到的文件描述符
- addr: 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen: 第二个参数结构体占的内存大小
详细了解一下这个sockaddr_in结构体:
struct sockaddr_in {
short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
struct in_addr sin_addr; // 4 字节 ,32位IP地址
char sin_zero[8]; // 8 字节 ,不使用
};
struct in_addr {
unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};
获得一个干净可用的 sockaddr_in 结构体后,可以正式绑定 IP 地址 和 端口号了。
代码呈现:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace nt_server
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR
};
// 端口号默认值
const uint16_t default_port = 8888;
class UdpServer
{
public:
// 构造
UdpServer(const std::string ip, const uint16_t port = default_port)
:port_(port), ip_(ip)
{}
// 析构
~UdpServer()
{}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_ == -1)
{
std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 创建成功
std::cout << "Create Success Socket: " << sock_ << std::endl;
// 2.绑定IP地址和端口号
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 置0
// 填充字段
local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
local.sin_port = htons(port_); // 主机序列转为网络序列
local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
// 绑定IP地址和端口号
int n = bind(sock_, (const sockaddr*)&local, sizeof(local));
if(n<0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
// 绑定成功
std::cout << "Bind IP&&Port Success" << std::endl;
}
// 启动服务器
void StartServer()
{}
private:
int sock_; // 套接字
uint16_t port_; // 端口号
std::string ip_; // IP地址(后面需要删除)
};
}
总结说明:
- 端口号会在网络里互相转发,需要把主机序列转换为网络序列,可以使用 htons 函数。
- 需要把点分十进制的字符串,转换为无符号短整数,可以使用 inet_addr 函数,这个函数在进行转换的同时,会将主机序列转换为网络序列(因为IP地址需要在网络里面发送)。
- 绑定IP地址和端口号这个行为并非直接绑定到当前主机中,而是在当前程序中,将创建的 socket 套接字,与目标IP地址与端口号进行绑定,当程序终止后,这个绑定关系也会随之消失。
2.2.3.3 地址转换函数 - 字符串和struct in_addr互相转换
我们这里为什么要使用字符串来表示IP地址:
首先大部分用户习惯使用的IP是点分十进制的字符串,就像下面这个这样。
128.11.3.31
我们的IP地址就是下面的第3个成员:
struct sockaddr_in {
short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
struct in_addr sin_addr; // 4 字节 ,32位IP地址
char sin_zero[8]; // 8 字节 ,不使用
};
struct in_addr {
unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};
点分十进制的IP地址不好输入,我们往往先用更好输入的字符串来存储IP地址,然后将字符串版的IP地址转换为struct in_addr版的IP地址(也就是点分十进制版的)。
字符串转 in_addr
结构体:
语法:
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
说明:
- 该函数将点分十进制的字符串表示的IPv4地址转换为网络字节序的32位整数。返回的是in_addr_t类型,通常用于填充sin_addr.s_addr字段。
- 这个函数是更通用的函数,支持IPv4和IPv6地址的转换。第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。第二个参数 src 是输入的字符串表示的IP地址,第三个参数 dst 是输出的二进制表示的IP地址。
使用:
#include <arpa/inet.h>
struct in_addr ipv4Address;
const char *ipString = "192.168.1.1";
inet_pton(AF_INET, ipString, &(ipv4Address.s_addr));
in_addr 结构体转字符串:
语法:
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
说明:
-
第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。
-
第二个参数 src 是输入的二进制表示的IP地址。
-
第三个参数 dst 是输出的字符串表示的IP地址的缓冲区。第四个参数 size 是缓冲区的大小。
使用:
#include <arpa/inet.h>
struct in_addr ipv4Address;
ipv4Address.s_addr = inet_addr("192.168.1.1");
char ipString[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(ipv4Address.s_addr), ipString, INET_ADDRSTRLEN);
我们让IP绑定到 0.0.0.0,0.0.0.0
表示任意IP地址:
#pragma once
//.....
namespace nt_server
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR
};
// 端口号默认值
const uint16_t default_port = 8888;
const std::string="0.0.0.0";//注意这里
class UdpServer
{
public:
// 构造
UdpServer(const std::string ip=defaultip, const uint16_t port = default_port)
:port_(port), ip_(ip)
{}
// 析构
~UdpServer()
{}
// 初始化服务器
void InitServer()
{
//。。。。
// 2.绑定IP地址和端口号
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 置0
// 填充字段
local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
local.sin_port = htons(port_); // 主机序列转为网络序列
local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
//。。。。
}
// 启动服务器
void StartServer()
{}
private:
int sock_; // 套接字
uint16_t port_; // 端口号
std::string ip_; // IP地址
};
}
server.cc
服务器源文件:
#include <memory> // 智能指针相关头文件
#include "server.hpp"
using namespace std;
using namespace nt_server;
int main()
{
unique_ptr<UdpServer> usvr(new UdpServer());
// 初始化服务器
usvr->InitServer();
// 启动服务器
usvr->StartServer();
return 0;
}
2.2.3.3 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);
解释说明:
- buf:接收缓冲区。
- len:接收缓冲区的大小。
- flags:默认设置为 0,表示阻塞。
- src_addr:输出型参数,获取客户端的套接字信息,也就是获取客户端的 ip 和端口号信息。因为是 udp 网络通信,所以这里传入的还是 struct sockaddr_in 类型的对象地址。
- addrlen:这里就是 struct sockaddr_in 对象的大小。
- 返回值:成功会返回获取到数据的字节数;失败返回 -1。
使用示例:
struct sockaddr_in sender;
socklen_t sender_len = sizeof(sender);
char buffer[1024];
int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&sender, &sender_len);
if (bytes_received < 0) {
perror("recvfrom failed");
// handle error
}
代码呈现:server.hpp
服务器头文件
namespace nt_server
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR
};
// 端口号默认值
const uint16_t default_port = 8888;
class UdpServer
{
//.....
// 启动服务器
void StartServer()
{
// 服务器是不断运行的,所以需要使用一个 while(true) 死循环
char buff[1024]; // 缓冲区
while (true)
{
// 1. 接收消息
struct sockaddr_in peer; // 客户端结构体
socklen_t len = sizeof(peer); // 客户端结构体大小
// 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
// 传入 0 表示当前是阻塞式读取
ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
buff[n] = '\0';
else
continue; // 继续读取
// 2.处理数据
std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
uint16_t clientPort = ntohs(peer.sin_port); // 获取端口号
printf("Server get message from [%s:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
// 3.回响给客户端
// ...
}
}
//.....
};
}
2.2.3.4 sendto - 向指定套接字中发送数据
语法:
#include <sys/types.h>
#include <sys/socket.h>
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:接收方的套接字信息,这里也就是客户端的套接字信息。
- addrlen: struct sockaddr_in 对象的大小。
使用示例:
struct sockaddr_in receiver;
receiver.sin_family = AF_INET;
receiver.sin_port = htons(12345); // Some port number
inet_pton(AF_INET, "192.168.1.1", &receiver.sin_addr); // Some IP address
char message[] = "Hello, World!";
ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0,
(struct sockaddr*)&receiver, sizeof(receiver));
if (bytes_sent < 0) {
perror("sendto failed");
// handle error
}
代码呈现:server.hpp
服务器头文件
//。。。
namespace nt_server
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR
};
// 端口号默认值
const uint16_t default_port = 8888;
class UdpServer
{
//.....
// 启动服务器
void StartServer()
{
// 服务器是不断运行的,所以需要使用一个 while(true) 死循环
char buff[1024]; // 缓冲区
while (true)
{
// 1. 接收消息
struct sockaddr_in peer; // 客户端结构体
socklen_t len = sizeof(peer); // 客户端结构体大小
// 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
// 传入 0 表示当前是阻塞式读取
ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
buff[n] = '\0';
else
continue; // 继续读取
// 2.处理数据
std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
uint16_t clientPort = ntohs(peer.sin_port); // 获取端口号
printf("Server get message from [%c:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
// 3.回响给客户端
n = sendto(sock_, buff, strlen(buff), 0, (const struct sockaddr *)&peer, sizeof(peer));
if (n == -1)
std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
}
}
//.....
};
}
如何证明服务器端正在运行:
可以通过 Linux
中查看网络状态的指令,因为我们这里使用的是 UDP
协议,所以只需要输入下面这条指令,就可以查看有哪些程序正在运行
netstat -nlup
修改 sever.cc 代码:
#include <memory> // 智能指针相关头文件
#include "server.hpp"
using namespace std;
using namespace nt_server;
int main()
{
unique_ptr<UdpServer> usvr(new UdpServer(80));//使用智能指针创建了一个UdeServer对象
// 初始化服务器
usvr->InitServer();
// 启动服务器
usvr->StartServer();
return 0;
}
2.2.3.5 命令行参数改装服务端
上面的代码中,我们的端口号都是在代码里面指定了的,但是我们不能每次使用的时候都去修改代码吧,我们其实通过命令行参数来指定端口号
server.hpp:
//....
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
//.....
server.cc:
#include <memory> // 智能指针相关头文件
#include "server.hpp"
using namespace std;
using namespace nt_server;
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
uint16_t port = stoi(argv[1]);//将字符串转换成端口号
unique_ptr<UdpServer> usvr(new UdpServer(port));//使用智能指针创建了一个UdeServer对象
// 初始化服务器
usvr->InitServer();
// 启动服务器
usvr->StartServer();
return 0;
}
2.3 Udp Client 客户端
因为一个端口号只能被一个进程 bind,客户端的应用是非常多的,如果在客户端采用静态 bind,那可能会出现两个应用同时 bind 同一个端口号,此时就注定了这两个应用一定是不能同时运行的。为了解决这个问题,一般不建议客户端 bind 一个固定的端口,而是由操作系统来进行动态的 bind,这样就可以避免端口号发生冲突。这也间接说明,对一个 client 端的进程来说,它的端口号是几并不重要,只要能够标识该进程在主机上的唯一性就可以。因为,一般都是由 clinet 端主动的向 server 端发送消息,所以 client 一定是能够知道 client 端的端口号。相反,服务器的端口号必须是确定的。因此,在编写客户端的代码时,第一步就是创建套接字,创建完无需进行 bind,直接向服务器发送数据,发送的时候,操作系统会为我们进行动态 bind。
client.hpp代码:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace nt_client
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
class UdpClient
{
public:
// 构造
UdpClient(const std::string& ip, uint16_t port)
:server_ip_(ip), server_port_(port)
{}
// 析构
~UdpClient()
{}
// 初始化客户端
void InitClient()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_ == -1)
{
std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Success Socket: " << sock_ << std::endl;
// 2.构建服务器的 sockaddr_in 结构体信息
bzero(&svr_, sizeof(svr_));
svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
svr_.sin_port = htons(server_port_); // 绑定服务器端口号
}
// 启动客户端
void StartClient()
{
char buff[1024];
while(true)
{
// 1.发送消息
std::string msg;
std::cout << "Input Message# ";
std::getline(std::cin, msg);
ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
if(n == -1)
{
std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
continue; // 重新输入消息并发送
}
// 2.接收消息
socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
if(n > 0)
buff[n] = '\0';
else
continue;
// 可以再次获取IP地址与端口号
std::string ip = inet_ntoa(svr_.sin_addr);
uint16_t port = ntohs(svr_.sin_port);
printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
}
}
private:
std::string server_ip_; // 服务器IP地址
uint16_t server_port_; // 服务器端口号
int sock_;
struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
};
}
client.cc代码:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace nt_client
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
class UdpClient
{
public:
// 构造
UdpClient(const std::string& ip, uint16_t port)
:server_ip_(ip), server_port_(port)
{}
// 析构
~UdpClient()
{}
// 初始化客户端
void InitClient()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_ == -1)
{
std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Success Socket: " << sock_ << std::endl;
// 2.构建服务器的 sockaddr_in 结构体信息
bzero(&svr_, sizeof(svr_));
svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
svr_.sin_port = htons(server_port_); // 绑定服务器端口号
}
// 启动客户端
void StartClient()
{
char buff[1024];
while(true)
{
// 1.发送消息
std::string msg;
std::cout << "Input Message# ";
std::getline(std::cin, msg);
ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
if(n == -1)
{
std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
continue; // 重新输入消息并发送
}
// 2.接收消息
socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
if(n > 0)
buff[n] = '\0';
else
continue;
// 可以再次获取IP地址与端口号
std::string ip = inet_ntoa(svr_.sin_addr);
uint16_t port = ntohs(svr_.sin_port);
printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
}
}
private:
std::string server_ip_; // 服务器IP地址
uint16_t server_port_; // 服务器端口号
int sock_;
struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
};
}
2.4 简易公共聊天室
server.hpp代码:
#pragma once
#include <iostream>
#include <string>
#include <functional>//注意这个
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<unordered_map>
namespace nt_server
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
// 端口号默认值
const uint16_t default_port = 8888;
using func_t = std::function<std::string(std::string)>;
// 可以简单的理解为func_t是一个参数为string,返回值同样为string的函数的类型
class UdpServer
{
public:
// 构造
UdpServer(const func_t& func, uint16_t port = default_port)//注意这里的func_t
:port_(port)
,serverHandle_(func)
//注意serverHandle_的类型已经是一个func_t,就是一个一个参数为string,返回值同样为string的函数的类型
{}
// 析构
~UdpServer()
{}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_ == -1)
{
std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 创建成功
std::cout << "Create Success Socket: " << sock_ << std::endl;
// 2.绑定IP地址和端口号
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 置0
// 填充字段
local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
local.sin_port = htons(port_); // 主机序列转为网络序列
// local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
local.sin_addr.s_addr = INADDR_ANY; // 绑定任何可用IP地址
// 绑定IP地址和端口号
if(bind(sock_, (const sockaddr*)&local, sizeof(local)))
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
// 绑定成功
std::cout << "Bind IP&&Port Success" << std::endl;
}
//检测是不是新用户
void CheckUser(const struct sockaddr_in & client,const std::string clientIp_,uint16_t clientPort_)
{
auto iter= online_user_.find(clientIp_);
if(iter==online_user_.end())
{
online_user_.insert({clientIp_,client});
std::cout<<"["<<clientIp_<<":"<<clientPort_<<"] add to oniline user."<<std::endl;
}
}
//广播给所有人
void Broadcast(const std::string& respond,const std::string clientIp_,uint16_t clientPort_)
{
for(const auto&usr :online_user_)
{
std::string message="[";
message+=clientIp_;
message+=" : ";
message+=std::to_string(clientPort_);
message+="]#";
message+=respond;
int z = sendto(sock_, respond.c_str(), respond.size(), 0, (const sockaddr*)&usr.second, sizeof(usr.second));
if(z == -1)
std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
}
}
// 启动服务器
void StartServer()
{
// 服务器是不断运行的,所以需要使用一个 while(true) 死循环
char buff[1024]; // 缓冲区
while(true)
{
// 1. 接收消息
struct sockaddr_in client; // 客户端结构体
socklen_t len = sizeof(client); // 客户端结构体大小
// 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
// 传入 0 表示当前是阻塞式读取
ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&client, &len);
if(n > 0)
buff[n] = '\0';
else
continue; // 继续读取
// 2.处理数据
std::string clientIp = inet_ntoa(client.sin_addr); // 获取用户的IP地址
uint16_t clientPort = ntohs(client.sin_port); // 获取端口号
//2.1.判断是不是新用户,如果是就加入,如果不是就什么也不做
CheckUser(client,clientIp,clientPort);
//2.2 对数据进行业务处理,并获取业务处理后的结果
std::string respond = serverHandle_(buff);
//特别注意这里,业务处理的代码已经放到了这个serverHandle_这个函数了,
// 3.回响给所有在线客户端
Broadcast(respond,clientIp,clientPort);
}
}
private:
int sock_; // 套接字
uint16_t port_; // 端口号
func_t serverHandle_; // 业务处理函数(回调函数)
std::unordered_map<std::string,struct sockaddr_in> online_user_;//在线用户列表
};
}
server.cc代码:
#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
using namespace std;
using namespace nt_server;
//业务处理函数
std::string ExecCommand(const std::string& request)
{
return request;
}
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
uint16_t port = stoi(argv[1]);//将字符串转换成端口号
unique_ptr<UdpServer> usvr(new UdpServer(ExecCommand,port));
// 初始化服务器
usvr->InitServer();
// 启动服务器
usvr->StartServer();
return 0;
}
client.hpp代码:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include<functional>
namespace nt_client
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
class UdpClient
{
public:
// 构造
UdpClient(const std::string &ip, uint16_t port)
: server_ip_(ip), server_port_(port)
{
}
// 析构
~UdpClient()
{
}
static void *send_message(void *argc)//传进来的是this指针
{
UdpClient*_this =(UdpClient*)argc;//强制转换为类指针
char buff[1024];
while (1)
{
// 1.发送消息
std::string msg;
std::cout << "Input Message# ";
std::getline(std::cin, msg);
ssize_t n = sendto(_this->sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr *)&_this->svr_, sizeof(_this->svr_));
if (n == -1)
{
std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
continue; // 重新输入消息并发送
}
}
return (void*)0;
}
static void *recv_message(void *argc)//传进来的是this指针
{
UdpClient*_this =(UdpClient*)argc;
char buff[1024];
while (1)
{
// 2.接收消息
socklen_t len = sizeof(_this->svr_); // 创建一个变量,因为接下来的参数需要传左值
int n = recvfrom(_this->sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&_this->svr_, &len);
if (n > 0)
buff[n] = '\0';
else
continue;
// 可以再次获取IP地址与端口号
std::string ip = inet_ntoa(_this->svr_.sin_addr);
uint16_t port = ntohs(_this->svr_.sin_port);
printf("Client get message from [%s:%d]# %s\n", ip.c_str(), port, buff);
}
return (void*)0;
}
// 初始化客户端
void InitClient()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ == -1)
{
std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Success Socket: " << sock_ << std::endl;
// 2.构建服务器的 sockaddr_in 结构体信息
bzero(&svr_, sizeof(svr_));
svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
svr_.sin_port = htons(server_port_); // 绑定服务器端口号
}
// 启动客户端
void StartClient()
{
pthread_t recv, sender;
//只需将类内的线程函数变化为static函数,但是static函数就不能直接访问到我们类内的私有数据,
//不要忘了这个线程函数的void*参数,我们可以把this指针作为参数传给它,
//然后就能通过这个this指针来访问到我们的类内的私有数据
pthread_create(&recv, nullptr, recv_message, (void*)this);
pthread_create(&sender, nullptr, send_message, (void*)this);
pthread_join(recv,nullptr);
pthread_join(sender,nullptr);
}
private:
std::string server_ip_; // 服务器IP地址
uint16_t server_port_; // 服务器端口号
int sock_;//套接字描述符
struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
};
}
client.cc保持不变
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace nt_client
{
// 退出码
enum
{
SOCKET_ERR = 1,
BIND_ERR,
USAGE_ERR
};
class UdpClient
{
public:
// 构造
UdpClient(const std::string& ip, uint16_t port)
:server_ip_(ip), server_port_(port)
{}
// 析构
~UdpClient()
{}
// 初始化客户端
void InitClient()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_ == -1)
{
std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Success Socket: " << sock_ << std::endl;
// 2.构建服务器的 sockaddr_in 结构体信息
bzero(&svr_, sizeof(svr_));
svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
svr_.sin_port = htons(server_port_); // 绑定服务器端口号
}
// 启动客户端
void StartClient()
{
char buff[1024];
while(true)
{
// 1.发送消息
std::string msg;
std::cout << "Input Message# ";
std::getline(std::cin, msg);
ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
if(n == -1)
{
std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
continue; // 重新输入消息并发送
}
// 2.接收消息
socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
if(n > 0)
buff[n] = '\0';
else
continue;
// 可以再次获取IP地址与端口号
std::string ip = inet_ntoa(svr_.sin_addr);
uint16_t port = ntohs(svr_.sin_port);
printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
}
}
private:
std::string server_ip_; // 服务器IP地址
uint16_t server_port_; // 服务器端口号
int sock_;
struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
};
}
makefile代码:
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11 -lpthread
client:client.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf server client
三、结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。