个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创计算机网络socket编程(1)_UDP网络编程实现echo server
收录于专栏【计算机网络】
本专栏旨在分享学习计算机网络的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
[功能介绍 :](#功能介绍 :)
[1. nocopy.hpp](#1. nocopy.hpp)
[2. InetAddr.hpp](#2. InetAddr.hpp)
[3. UdpServer.hpp](#3. UdpServer.hpp)
[4. UdpServerMain.cc](#4. UdpServerMain.cc)
[5. UdpClientMain.cc](#5. UdpClientMain.cc)
[6. 效果展示](#6. 效果展示)
功能介绍 :
简单的回显服务器和客服端代码, 能接受多个客服端的代码.
1. nocopy.hpp
cpp
#pragma once
class nocopy
{
public:
nocopy(){}
~nocopy(){}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
定义一个 nocopy 的类, 通过C++11 delete关键字的特性, 阻止该类的拷贝构造和拷贝赋值.
比如下面的例子都是不可以的 :
cpp
nocopy obj1;
nocopy obj2 = obj1; // 错误:拷贝构造函数被删除
cpp
nocopy obj1;
nocopy obj2;
obj2 = obj1; // 错误:拷贝赋值运算符被删除
为什么需要禁止对象的拷贝呢?
这样的设计常见于以下情况 :
资源管理类 : 例如, 涉及底层资源 (如文件句柄, 网络连接等) 的类通常不希望被复制, 因为这些资源可能会导致资源管理上的混乱或错误~
不想允许拷贝的类: 有些类可能设计上不希望被拷贝, 例如类的状态可能是唯一的, 拷贝它没有意义~~
我们这里显然是第一种~~
2. InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class InetAddr
{
private:
void ToHost(const struct sockaddr_in &addr)
{
_port = ntohs(addr.sin_port);
_ip = inet_ntoa(addr.sin_addr);
}
public:
InetAddr(const struct sockaddr_in &addr):_addr(addr)
{
ToHost(addr);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{
}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
InetAddr 类它封装了网络地址 (IP 端口), 以及提供了访问这些信息的方法, 该类通过了 struct sockaddr_in 来存储 IP 地址和端口, 并提供了转换和获取信息的功能
成员变量 :
_ip: 存储 IP 地址, 使用 std::string 类型, 因为 IP 地址通常表示一个点分十进制的字符串
_port: 存储端口号, 使用 uint16_t 类型, 端口号是一个16位的无符号整数
_addr: 存储原始的 struct sockaddr_in 结构体, 它包含了 IP 地址和端口信息
ToHost():
**ToHost() :**将 struct sockaddr_in 中的网络字节数据转换为主机字节序, 并提取 IP 和端口.
**ntohs(addr.sin_port) :**将网络字节序的端口号转换为主机字节序
**inet_ntoa(addr.sin_addr) :**将网络字节序的 IP 地址转换为点分十进制的字符串的形式
构造函数 :
构造函数 : 接受一个 struct sockaddr_in 类型的参数, 表示网络地址
构造函数初始化 _addr 成员, 存储传入的地址
然后调用 ToHost() 方法来从该地址中提取 IP 和端口, 并进行转换
Ip() && Port()
Ip() : 返回存储的 IP 地址, 类型为 std::string
Port() : 返回存储的端口号, 类型为 uint16_t
析构函数 :
由于没有动态分配资源, 所以没有必要进行额外的清理工作~
3. UdpServer.hpp
cpp
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace log_ns;
static const int gsockfd = -1;
static const uint16_t glocalport = 8888;
enum
{
SOCKET_ERROR = 1,
BIND_ERROR
};
// UdpServer user("192.1.1.1", 8899);
// 一般服务器主要是用来进行网络数据读取和写入的。IO的
// 服务器IO逻辑 和 业务逻辑 解耦
class UdpServer : public nocopy
{
public:
UdpServer(uint16_t localport = glocalport)
: _sockfd(gsockfd),
_localport(localport),
_isrunning(false)
{
}
void InitServer()
{
// 1. 创建socket文件
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(FATAL, "socket error\n");
exit(SOCKET_ERROR);
}
LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 3
// 2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_localport);
// local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1. 需要4字节IP 2. 需要网络序列的IP -- 暂时
local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定
int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "bind error\n");
exit(BIND_ERROR);
}
LOG(DEBUG, "socket bind success\n");
}
void Start()
{
_isrunning = true;
char inbuffer[1024];
while (_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
InetAddr addr(peer);
inbuffer[n] = 0;
std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuffer << std::endl;
std::string echo_string = "[udp_server echo] #";
echo_string += inbuffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
std::cout << "recvfrom , error" << std::endl;
}
}
}
~UdpServer()
{
if(_sockfd > gsockfd) ::close(_sockfd);
}
private:
int _sockfd;
uint16_t _localport;
// std::string _localip; // TODO:后面专门要处理一下这个IP
bool _isrunning;
};
这段代码定义了一个 UdpServer 类, 用于创建和管理一个 UDP 服务器, 它能够接受客户端的消息并发送相应~
1. 常量和枚举
**gsockfd :**设置为 -1, 用作初始的 sockfd, 代表一个无效的套接字描述符
**glocalport :**服务器的默认接口, 默认值为 8888
SOCKET_ERROR和 BIND_ERROR: 自定义的错误代码, 分别代表套接字创建失败和绑定失败.
2. 成员变量
_sockfd: 存储套接字文件描述符
_localport: 服务器绑定的本地端口号
_isrunning: 一个布尔值, 指示服务器是否正在运行
3. 构造函数
UdpServer 类的构造函数的初始化了以下成员变量 :
**_sockfd :**默认初始化为 gsockfd (-1), 表示无效的套接字
_localport : 使用传入的端口号 localport 或默认端口 glocalport (8888)
isrunning : 初始化为 false , 表示服务器的初始状态是未运行.
4. InitServer()
创建套接字 : 使用 socket() 函数创建一个 UDP 套接字, AF_INET 表示使用 IPv4 协议, SOCK_DGRAM 表示使用 UDP.
-
如果创建失败, 记录日志并退出
-
成功后, 输出日志, 显示套接字描述符 _sockfd.
绑定套接字 : 使用 bind() 函数将套接字与本地地址 (IP 和端口) 绑定
local.sin_family = AF_INET : 设置为 IPv4 地址族
**local.sin_port = htons(_localport) :**将端口号转换为网络字节序
**local.sin_addr.s_addr = INADDR_ANY :**服务器绑定到任意 IP 地址
**band()**调用套接字与本地地址绑定, 如果绑定失败, 记录错误日志并退出
5. Start()
Start() 方法用于接收数据并发送响应
**_isrunning = true :**启动服务器, 进入接收数据的循环
**recvfrom() :**接收 UDP 数据包, 数据存放在 inbuffer 中, peer 存储发送方的地址信息
如果接收成功 (n > 0), 则:
-
创建一个 InetAddr 对象, 解析发送对方的 IP 地址和端口
-
输出接收到的消息及其来源 IP 和端口
-
生成一个响应消息 ("[udp_server echo]#" + 接收的消息), 然后使用 sendto() 将其发送回客户端
如果 recvfrom() 失败, 打印错误消息.
6. 析构函数
在 UdpServer 对象销毁时, 关闭套接字 _sockfd, 如果 _sockfd 大于 gsockfd (即有效套接字), 则调用 close() 关闭套接字
4. UdpServerMain.cc
cpp
#include "UdpServer.hpp"
#include <memory>
// ./udp_server local-port
// ./udp_server 8888
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
EnableScreen();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); //C++14的标准
usvr->InitServer();
usvr->Start();
return 0;
}
std::stoi(argv[1]) : 将命令行参数中的端口号字符串 (argv[1]) 转换为 uint6_t 类型 (无符号16位整数), std::stoi 是 C++ 标准库函数, 用于将字符串转换为整数, 如果 argv[1] 不是一个有效的数组, std::stoi 将抛出异常
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port) :
这行代码使用了 C++14 引入的 std::make_unique, 它用来创建一个 std::unique_ptr, 智能指针类型, 用于管理动态分配的 UdpServer 对象.
std::make_unique<UdpServer>(port) : 动态创建一个 UdpServer 对象, 并将端口号 port 传递给它的构造函数.
std::unique_ptr<UdpServer> usvr : usvr 是一个智能指针, 指向创建的 Udpserver 对象, unique_ptr 确保在 usvr 离开作用域时自动销毁对象, 避免内存泄露.
UdpServer 类接收一个端口号作为构造参数, 表示该服务器将在指定的端口上运行
然后便直接 初始化服务器 和 启动服务
5. UdpClientMain.cc
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 客户端在未来一定要知道服务器的IP地址和端口号
// ./udp_client server-ip server-port
// ./udp_client 127.0.0.1 8888
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
// client的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢?
// client 需要 bind它自己的IP和端口, 但是client 不需要 "显示" bind它自己的IP和端口,
// client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while(1)
{
std::string line;
std::cout << "Please Enter# ";
std::getline(std::cin, line);
// std::cout << "line message is@ " << line << std::endl;
int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server)); // 你要发送消息,你得知道你要发给谁啊!
if(n > 0)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
else
{
std::cout << "recvfrom error" << std::endl;
break;
}
}
else
{
std::cout << "sendto error" << std::endl;
break;
}
}
::close(sockfd);
return 0;
}
-
从命令行获取目标服务器的 IP 地址和端口号。
-
创建 UDP 套接字并配置服务器的地址。
-
进入循环,等待用户输入数据。
-
将用户输入的数据通过 sendto 发送给服务器。
-
接收服务器的响应数据并输出到控制台。
-
出现错误时终止通信并关闭套接字。