当前位置: 首页 > news >正文

TCP-单线程版本

TCP-单线程版本

创建几个文件如下:

在这里插入图片描述

Makefile:

.PHONY:all
all:tcpclient tcpservertcpclient:TcpClient.ccg++ -o $@ $^ -std=c++17
tcpserver:TcpServer.ccg++ -o $@ $^ -std=c++17 -lpthread.PHONY:clean
clean:rm -f tcpclient tcpserver

在TcpServer.hpp中建立框架:

class TcpServer
{public:TcpServer(){}//初始化void Init(){}//启动void Run(){}~TcpServer(){}private:
};

创建一个 TCP 服务器

void Usage(std::string proc)
{std::cerr<<"Usage:"<<proc<<"port"<<std::endl;
}
//./tcpserver port
int main(int argc,char*argv[])
{if(argc!=2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port=std::stoi(argv[1]);return 0;
}

它主要包含以下功能:

  1. Usage 函数

    • 用于打印程序的正确使用方法
    • 接收一个字符串参数 proc(通常是程序名)
    • 向标准错误输出打印使用格式:Usage:程序名 port
  2. main 函数

    • 检查命令行参数数量是否正确(要求恰好 2 个参数)
    • 如果参数数量不对(不是 2 个),调用 Usage 函数显示用法并退出程序
    • 将第二个参数(端口号)从字符串转换为 uint16_t 类型的整数
  3. 程序目的

    • 这是一个 TCP 服务器的启动程序框架
    • 程序预期用法:./tcpserver port(port 是要监听的端口号)
    • 当前代码只是完成了参数检查,还没有实际创建服务器
  4. 注意

    • 程序目前只是获取了端口号,没有后续的服务器创建和监听代码
    • std::stoi 用于字符串到整数的转换

这段代码是一个 TCP 服务器程序的初始框架

创建一个文件用来记录错误码和禁止拷贝

Common.hpp

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <iostream>
enum  ExitCode
{OK=0,USAGE_ERR,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};class NoCopy
{public:NoCopy(){}~NoCopy(){}NoCopy(const NoCopy&)=delete;const NoCopy&operator=(const NoCopy&)=delete;private:};
#define CONV(addr) ((struct sockaddr*)&addr)

初始化:

步骤1:创建套接字

			_listensockfd=socket(AF_INET,SOCK_STREAM,0);if(_listensockfd<0){LOG(LogLevel::FATAL)<<"socket error";exit(SOCKET_ERR);}LOG(LogLevel::INFO)<<"socket success"<<_listensockfd;

注意点:TCP用的是SOCK_STREAM

步骤2:绑定地址和端口

InetAddr local(_port);int n=bind(_listensockfd,local.NetAddrPtr(),local.NetAddrLen());if(n<0){LOG(LogLevel::FATAL)<<"bind error";exit(BIND_ERR);}LOG(LogLevel::INFO)<<"bind success"<<_listensockfd;

步骤3:开始监听

			const int backlog = 5;  // 等待连接队列的最大长度n=listen(_listensockfd,backlog);if(n<0){LOG(LogLevel::FATAL)<<"listen error";exit(LISTEN_ERR);}LOG(LogLevel::INFO)<<"listen success"<<_listensockfd;

bind()TCP/UDP 网络编程 中用于将 Socket 绑定到指定 IP 地址和端口 的关键系统调用。

1. bind() 函数原型

#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明

参数说明
sockfd要绑定的 Socket 文件描述符(由 socket() 创建)
addr指向 sockaddr 结构体的指针(存储 IP 和端口)
addrlenaddr 结构体的长度(sizeof(sockaddr_in)

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno(如 EADDRINUSE 表示端口被占用)

2. bind() 的典型使用场景

(1) TCP 服务器绑定固定端口

int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 创建 TCP Socketstruct sockaddr_in addr;
addr.sin_family = AF_INET;                     // IPv4
addr.sin_port = htons(8080);                   // 绑定 8080 端口
addr.sin_addr.s_addr = htonl(INADDR_ANY);      // 监听所有网卡(0.0.0.0)if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {perror("bind failed");exit(1);
}
  • INADDR_ANY:监听所有可用网卡(0.0.0.0
  • htons():将端口号从 主机字节序 转为 网络字节序(避免大小端问题)

(2) UDP 服务器绑定端口

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);   // 创建 UDP Socketstruct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(12345);                  // 绑定 12345 端口
addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 绑定特定 IPif (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {perror("bind failed");exit(1);
}
  • inet_addr("192.168.1.100"):绑定到特定 IP 地址(仅接收该网卡的数据)

3. bind() 常见错误

错误码 (errno)原因解决方案
EADDRINUSE端口已被占用换一个端口,或设置 SO_REUSEADDR
EACCES无权限绑定特权端口(<1024)改用 sudo 或绑定更高端口
EINVALSocket 已绑定过确保 bind() 只调用一次

如何避免 EADDRINUSE(端口占用)?

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 允许端口复用
bind(sockfd, ...);
  • SO_REUSEADDR 允许 快速重启服务器(避免 TIME_WAIT 状态阻塞)

4. bind() vs connect()

函数用途适用协议
bind()服务器 绑定 IP:PortTCP/UDP
connect()客户端 连接服务器TCP(UDP 也可用,但不建立连接)

TCP 客户端通常不需要 bind()

// 客户端一般只需 connect(),系统自动分配端口
connect(sockfd, server_addr, sizeof(server_addr));

UDP 客户端可以 bind() 固定端口

// UDP 客户端如果想固定源端口,可以 bind()
bind(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));

5. 完整 TCP 服务器示例

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>int main() {// 1. 创建 Socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {perror("socket failed");return 1;}// 2. 设置 SO_REUSEADDRint opt = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 3. 绑定 IP:Portstruct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(8080);          // 绑定 8080 端口addr.sin_addr.s_addr = INADDR_ANY;    // 0.0.0.0if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {perror("bind failed");close(sockfd);return 1;}// 4. 监听并接受连接(略)listen(sockfd, 5);// accept()...close(sockfd);return 0;
}

总结

  • bind() 用于 服务器 绑定 IP 和端口。
  • TCP 服务器必须 bind(),客户端通常不用
  • UDP 服务器/客户端都可以 bind()
  • 常见错误:EADDRINUSE(端口占用),可用 SO_REUSEADDR 解决。
  • 绑定 INADDR_ANY0.0.0.0)表示监听所有网卡。

2.处理客户端连接

void Service(int sockfd, InetAddr& peer)
{char buffer[1024];while(true){// 1. 读取数据ssize_t n = read(sockfd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = '\0';  // 正确终止字符串LOG(LogLevel::DEBUG) << peer.StringAddr() << " say: " << buffer;// 2. 回显数据std::string echo_string = "echo#";echo_string += buffer;if (write(sockfd, echo_string.c_str(), echo_string.size()) < 0){LOG(LogLevel::WARNING) << "write error: " << strerror(errno);break;}}else if(n == 0)  // 客户端关闭连接{LOG(LogLevel::INFO) << peer.StringAddr() << " disconnected";break;}else  // 读取错误{if (errno == EINTR) continue;  // 被信号中断,继续读取LOG(LogLevel::WARNING) << peer.StringAddr() << " read error: " << strerror(errno);break;}}close(sockfd);  // 关闭客户端socket
}

3.启动服务器

// 启动服务器
void Run()
{_isrunning = true;while(_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接受新连接int sockfd = accept(_listensockfd, CONV(peer), &len);if(sockfd < 0){if (errno == EINTR) continue;  // 被信号中断,继续acceptLOG(LogLevel::ERROR) << "accept error: " << strerror(errno);continue;}// 处理新连接InetAddr addr(peer);LOG(LogLevel::INFO) << "new connection from: " << addr.StringAddr();Service(sockfd, addr);  // 注意:这里是单线程处理,会阻塞其他连接}
}

需要包含的头文件:

InetAddr.hpp

class InetAddr
{public:InetAddr(struct sockaddr_in &addr) : _addr(addr),_ip("0"){_port = ntohs(_addr.sin_port);           // 从网络中拿到的!网络序列//_ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IPchar ipbuffer[64];inet_ntop(AF_INET,&addr.sin_addr,ipbuffer,sizeof(ipbuffer));_ip=ipbuffer;}InetAddr(const std::string &ip,uint16_t port):_ip(ip),_port(port){memset(&_addr,0,sizeof(_addr));_addr.sin_family=AF_INET;_addr.sin_addr.s_addr=INADDR_ANY;inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);_addr.sin_port=htons(_port);}InetAddr(uint16_t port):_port(port){memset(&_addr,0,sizeof(_addr));_addr.sin_family=AF_INET;inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);_addr.sin_port=htons(_port);}uint16_t Port() {return _port;}std::string Ip() {return _ip;}const struct sockaddr_in &NetAddr() { return _addr; }const struct sockaddr*NetAddrPtr(){return CONV(_addr);}socklen_t NetAddrLen(){return sizeof(_addr);}bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:struct sockaddr_in _addr;//原始网络地址结构体std::string _ip;//字符串格式的IP地址uint16_t _port;//主机字节序的端口号
};

这个 InetAddr 类是一个 网络地址封装类,主要用于 IPv4 地址和端口的存储、转换和操作,适用于 TCP/UDP 网络编程。它封装了 sockaddr_in 结构体,并提供了一系列便捷的方法来管理 IP 地址和端口。

1. 核心功能

(1) 构造方法

构造函数用途
InetAddr(struct sockaddr_in &addr)sockaddr_in 构造(用于 accept() 获取的客户端地址)
InetAddr(const std::string &ip, uint16_t port)字符串IP + 端口 构造(如 "192.168.1.1", 8080
InetAddr(uint16_t port)仅指定端口(IP 默认 0.0.0.0,即 INADDR_ANY

(2) 主要方法

方法用途
Port()返回 主机字节序 的端口号
Ip()返回 字符串格式 的 IP 地址(如 "192.168.1.1"
NetAddr()返回 sockaddr_in 结构体(用于 bind()/connect()
NetAddrPtr()返回 sockaddr*(用于 accept()/bind()
NetAddrLen()返回 sockaddr_in 的大小(用于 accept()/bind()
StringAddr()返回 "IP:Port" 格式的字符串(如 "192.168.1.1:8080"
operator==比较两个 InetAddr 是否相同

2. 关键实现细节

(1) sockaddr_instd::string _ip 的转换

  • inet_ntop()(网络字节序 → 字符串):
    char ipbuffer[64];
    inet_ntop(AF_INET, &addr.sin_addr, ipbuffer, sizeof(ipbuffer));
    _ip = ipbuffer;  // 如 "192.168.1.1"
    
  • inet_pton()(字符串 → 网络字节序):
    inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
    

(2) 端口字节序转换

  • htons()(主机 → 网络字节序):
    _addr.sin_port = htons(_port);  // 用于 `bind()`/`connect()`
    
  • ntohs()(网络 → 主机字节序):
    _port = ntohs(_addr.sin_port);  // 用于解析 `accept()` 返回的地址
    

(3) 默认 IP (INADDR_ANY)

  • 如果未指定 IP,默认绑定 0.0.0.0(监听所有网卡):
    _addr.sin_addr.s_addr = INADDR_ANY;
    

3. 典型使用场景

(1) TCP 服务器绑定端口

InetAddr addr(8080);  // 绑定 0.0.0.0:8080
bind(sockfd, addr.NetAddrPtr(), addr.NetAddrLen());

(2) TCP 客户端连接服务器

InetAddr server_addr("192.168.1.100", 8080);
connect(sockfd, server_addr.NetAddrPtr(), server_addr.NetAddrLen());

(3) 获取客户端地址(accept()

struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);InetAddr peer_addr(client_addr);  // 解析客户端 IP:Port
std::cout << "Client connected: " << peer_addr.StringAddr() << std::endl;

5. 总结

  • InetAddr 封装了 sockaddr_in,简化了 IP/Port 的转换和管理
  • 适用于 bind()connect()accept() 等 Socket 操作
  • 提供 StringAddr() 方便日志输出
  • 可扩展性(如支持 IPv6、更好的错误处理)。

r.NetAddrLen());


**(3) 获取客户端地址(`accept()`)**```cpp
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &len);InetAddr peer_addr(client_addr);  // 解析客户端 IP:Port
std::cout << "Client connected: " << peer_addr.StringAddr() << std::endl;

5. 总结

  • InetAddr 封装了 sockaddr_in,简化了 IP/Port 的转换和管理
  • 适用于 bind()connect()accept() 等 Socket 操作
  • 提供 StringAddr() 方便日志输出
  • 可扩展性(如支持 IPv6、更好的错误处理)。

这是一个 轻量级、实用的网络地址工具类,适合嵌入到 TCP/UDP 服务器或客户端代码中。

http://www.lryc.cn/news/608577.html

相关文章:

  • pytorch 安装
  • 2025年渗透测试面试题总结-2025年HW(护网面试) 76-1(题目+回答)
  • cmd怎么取消关机命令
  • 麦肯锡咨询公司PEI经典面试题目汇总
  • 【一天一个知识点】RAG遇见推理
  • Piriority_queue
  • sifu mod制作 相关经验
  • Linux性能监控与调优全攻略
  • 轻量级鼠标右键增强工具 MousePlus
  • 轨道追逃博弈仿真
  • FreeRTOS源码分析二:task启动(RISCV架构)
  • 断路器瞬时跳闸曲线数据获取方式
  • Codeforces Round 1039 (Div. 2) A-C
  • 搜索引擎评估革命:用户行为模型如何颠覆传统指标?
  • Pytorch-02数据集和数据加载器的基本原理和基本操作
  • Node.js 路由与中间件
  • DyWA:用于可推广的非抓握操作的动态自适应世界动作模型
  • 浅拷贝与深拷贝的区别
  • 技术面试知识点详解 - 从电路到编程的全栈面经
  • 机试备考笔记 2/31
  • linux编译基础知识-头文件标准路径
  • 系统思考:超越线性分析
  • SpringBoot相关注解
  • MybatisPlus-逻辑删除
  • c++之基础B(进制转换)(第三课)
  • ARP协议是什么?ARP欺骗是如何实现的?我们该如何预防ARP欺骗?
  • 存储过程的介绍、基本语法、delimiter的使用
  • HarmonyOS 开发:基于 ArkUI 实现复杂表单验证的最佳实践
  • Makefile 从入门到精通:自动化构建的艺术
  • 【设计模式】 3.设计模式基本原则