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;
}
它主要包含以下功能:
-
Usage 函数:
- 用于打印程序的正确使用方法
- 接收一个字符串参数
proc
(通常是程序名) - 向标准错误输出打印使用格式:
Usage:程序名 port
-
main 函数:
- 检查命令行参数数量是否正确(要求恰好 2 个参数)
- 如果参数数量不对(不是 2 个),调用 Usage 函数显示用法并退出程序
- 将第二个参数(端口号)从字符串转换为 uint16_t 类型的整数
-
程序目的:
- 这是一个 TCP 服务器的启动程序框架
- 程序预期用法:
./tcpserver port
(port 是要监听的端口号) - 当前代码只是完成了参数检查,还没有实际创建服务器
-
注意:
- 程序目前只是获取了端口号,没有后续的服务器创建和监听代码
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 和端口) |
addrlen | addr 结构体的长度(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 或绑定更高端口 |
EINVAL | Socket 已绑定过 | 确保 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:Port | TCP/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_ANY
(0.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_in
和 std::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 服务器或客户端代码中。