文章目录
- [Echo server](#Echo server)
-
- [1.1 功能介绍](#1.1 功能介绍)
- [1.2 TCP核心接口](#1.2 TCP核心接口)
-
- [1. socket():创建套接字(通信端点)](#1. socket():创建套接字(通信端点))
- [2. bind():绑定地址和端口](#2. bind():绑定地址和端口)
- [3. listen():监听连接(仅服务器端)](#3. listen():监听连接(仅服务器端))
- [4. accept():接受连接(仅服务器端)](#4. accept():接受连接(仅服务器端))
- [5. connect():发起连接(仅客户端)](#5. connect():发起连接(仅客户端))
- [6. read()/write() / recv()/send():收发数据](#6. read()/write() / recv()/send():收发数据)
- [7. close():关闭连接](#7. close():关闭连接)
- [8. 总结](#8. 总结)
- [1.3 demo1 v1](#1.3 demo1 v1)
- [1.4 实验结果](#1.4 实验结果)
- [1.5 流程详解](#1.5 流程详解)
-
- [1.5.1 关于listen() 函数的核心作用](#1.5.1 关于listen() 函数的核心作用)
-
- [1. 标准原型](#1. 标准原型)
- [2. 参数详解](#2. 参数详解)
- [3. 返回值](#3. 返回值)
- [4. 关键注意事项](#4. 关键注意事项)
- [5. listen() 调用后的套接字状态变化](#5. listen() 调用后的套接字状态变化)
- [6. 总结](#6. 总结)
- [1.5.2 accept() 函数的核心作用](#1.5.2 accept() 函数的核心作用)
- [1.5.3 connect() 函数的核心作用](#1.5.3 connect() 函数的核心作用)
- [demo1 v2](#demo1 v2)
- [demo1 v3](#demo1 v3)
- demo2
本文主要是关于UDP网络通信编程的两个demo,通过这两个demo完全掌握UDP网络通信。
Echo server
1.1 功能介绍
功能:简单的回显服务器和客戶端代码,客户端向服务器发送消息,服务器接受消息之后向客户端回显。
1.2 TCP核心接口
TCP 是面向连接的可靠传输协议,基于它的网络编程(以 Linux/Unix 环境为例)遵循经典的 C/S(客户端/服务器)模型,核心接口围绕"创建套接字-绑定地址-监听-接受连接-收发数据-关闭连接"这一流程展开。下面按使用顺序逐一介绍核心接口:
1. socket():创建套接字(通信端点)
这是所有网络编程的第一步,用于创建一个"套接字文件描述符",相当于为网络通信打开一个"通道"。
-
函数原型 :
c#include <sys/socket.h> int socket(int domain, int type, int protocol); -
参数说明 :
domain:地址族,TCP 用AF_INET(IPv4)或AF_INET6(IPv6);type:套接字类型,TCP 必须用SOCK_STREAM(流式套接字,对应面向连接的可靠传输);protocol:协议类型,TCP 填0(系统会自动匹配SOCK_STREAM对应的 TCP 协议)。
-
返回值 :成功返回非负的文件描述符,失败返回
-1。 -
示例 :
c// 创建TCP套接字(IPv4) int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket failed"); // 错误提示 exit(1); }
2. bind():绑定地址和端口
将创建的套接字与服务器的 IP 地址、端口号绑定,让客户端能找到对应的服务器。
-
函数原型 :
cint bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); -
参数说明 :
sockfd:socket()返回的套接字描述符;addr:指向包含 IP 和端口的地址结构体(IPv4 用struct sockaddr_in);addrlen:地址结构体的长度。
-
示例 :
cstruct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); // 初始化结构体 server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有本机IP server_addr.sin_port = htons(8080); // 绑定8080端口(htons转换字节序) if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind failed"); close(sockfd); exit(1); }
3. listen():监听连接(仅服务器端)
将绑定后的套接字转为"监听套接字",开始等待客户端的连接请求。
-
函数原型 :
cint listen(int sockfd, int backlog); -
参数说明 :
sockfd:绑定后的套接字描述符;backlog:未处理的连接请求队列的最大长度(如5,现代系统会自动调整)。
-
示例 :
cif (listen(sockfd, 5) == -1) { perror("listen failed"); close(sockfd); exit(1); } printf("Server listening on port 8080...\n");
4. accept():接受连接(仅服务器端)
从监听队列中取出一个已完成的客户端连接,返回一个新的套接字描述符(专门用于和该客户端通信),原监听套接字仍继续监听其他连接。
-
函数原型 :
cint accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); -
参数说明 :
sockfd:监听套接字描述符;addr:输出参数,存储客户端的 IP 和端口信息(可填NULL表示不关心);addrlen:输入输出参数,传入addr结构体的长度,返回实际使用的长度。
-
示例 :
cstruct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); // 阻塞等待客户端连接 int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); if (connfd == -1) { perror("accept failed"); close(sockfd); exit(1); } // 打印客户端信息 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); printf("Client connected: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
5. connect():发起连接(仅客户端)
客户端通过该接口向服务器发起 TCP 连接请求,完成三次握手。
-
函数原型 :
cint connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); -
参数说明 :
sockfd:客户端创建的套接字描述符;addr:服务器的 IP 和端口地址结构体;addrlen:地址结构体长度。
-
示例 :
cint client_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); // 将服务器IP(如127.0.0.1)转为网络字节序 inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("connect failed"); close(client_fd); exit(1); } printf("Connected to server successfully!\n");
6. read()/write() / recv()/send():收发数据
连接建立后,服务器和客户端通过 accept()/connect() 返回的套接字描述符收发数据:
-
read()/write():通用的文件读写接口,可直接用于套接字(因为套接字是文件描述符); -
recv()/send():专门的网络数据收发接口,支持额外参数(如MSG_NOSIGNAL避免断连触发信号)。 -
示例(服务器接收+发送) :
cchar buf[1024]; // 接收客户端数据 ssize_t n = read(connfd, buf, sizeof(buf)-1); if (n <= 0) { perror("read failed"); close(connfd); return; } buf[n] = '\0'; // 加字符串结束符 printf("Received from client: %s\n", buf); // 向客户端发送响应 const char *resp = "Hello from server!"; write(connfd, resp, strlen(resp));
7. close():关闭连接
释放套接字描述符,终止 TCP 连接(会触发四次挥手)。
-
函数原型 :
cint close(int fd); -
示例 :
cclose(connfd); // 关闭和客户端的通信套接字 close(sockfd); // 关闭服务器的监听套接字
8. 总结
- TCP 编程核心流程:服务器
socket() → bind() → listen() → accept() → 收发数据 → close();客户端socket() → connect() → 收发数据 → close()。 - 关键区分:
listen()仅标记套接字为"监听状态",accept()才真正接收连接并返回新的通信套接字(原监听套接字可复用)。 - 核心接口的作用:
socket创建通道,bind绑定地址,listen开启监听,accept/connect建立连接,read/write传输数据,close释放资源。
1.3 demo1 v1
v1版本仅能实现单进程连接访问服务器,仅仅是作为框架演示。
server.hpp
cpp
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
{
}
void Init()
{
// 1. 创建套接字,socket类比open
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;
// 2. bind端口号
InetAddr local(_port);
int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bing error";
exit(BIND_ERR);
}
LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;
// 3. 设置socket状态
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
}
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设置成C风格的字符串
buffer[n] = 0;
LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
// 2. 写回数据
std::string echo_string = buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
close(sockfd);
break;
}
else
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
close(sockfd);
break;
}
}
}
void Run()
{
_isrunning = true;
while (_isrunning)
{
// 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
int sockfd = accept(_listensockfd, CONV(peer), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
InetAddr addr(peer);
LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();
Service(sockfd, addr);
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
};
server.cc
cpp
#include "TcpServer.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << "port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
tsvr->Init();
tsvr->Run();
return 0;
}
client.hpp
cpp
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(SOCKET_ERR);
}
// 2. 发起连接
InetAddr serveraddr(serverip, serverport);
int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());
if(n < 0)
{
std::cerr << "connect error" << std::endl;
exit(CONNECT_ERR);
}
// 3. echo client
while(true)
{
std::string line;
std::cout << "Please Enter@ ";
std::getline(std::cin, line);
write(sockfd, line.c_str(), line.size());
char buffer[1024];
ssize_t size = read(sockfd, buffer, sizeof(buffer)-1);
if(size > 0)
{
buffer[size] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
close(sockfd);
return 0;
}
InetAddr.hpp
cpp
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类
class InetAddr
{
public:
InetAddr() {}
InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
_port = ntohs(_addr.sin_port);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
_ip = ipbuffer;
}
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _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);
}
InetAddr(uint16_t port) : _port(port), _ip()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_addr.s_addr = INADDR_ANY;
_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;
uint16_t _port;
};
1.4 实验结果

1.5 流程详解
先看服务端代码

先判断运行服务器方式是否正确。

使用智能指针创建服务器对象,传参数就是为服务器指定的端口。
执行初始化方法。

先搂一眼服务器的成员变量,包括端口号,监听文件描述符,运行状态。
ok紧着执行初始化方法

首先创建套接字,这里的第二个参数变成了SOCK_STREAM表示流式套接字,是TCP专用。
判断是否创建成功,
输出日志信息。

这里还封装了common类,用来表示退出信息。

手动绑定端口号,绑定之前要先填写socket信息,这里专门封装了InetAddr类完成。
关于InetAddr类:

三个参数就是socket信息

重载的四个构造函数,这里用的最后一个:
清空结构体,填写家族协议,IP,端口号。
对于IP不需要手动绑定,服务器可能有多个网卡(有线/无线/内网/外网IP),绑特定IP只会响应该网卡的请求,其他网卡的请求会被丢弃;绑INADDR_ANY能接收所有网卡的请求。
对于端口号,需要将本地主机序列转化为网络序列。


这里对强转做了包装,方便bind函数的第二三参数的填写。
判断是否绑定成功,用日志输出。
以上和UDP其实都差不多...

TCP的特点是面向连接,就由这里的listen来体现。
1.5.1 关于listen() 函数的核心作用
listen() 是专属于TCP 服务器端 的系统调用,它的核心作用是:
将一个已经绑定(bind())了 IP 和端口的套接字(sockfd),从"主动套接字"转为"被动监听套接字",并告诉操作系统内核:该套接字准备好接受客户端的连接请求了。
简单来说,listen() 是服务器"开启监听"的关键一步,调用后服务器才会真正开始等待客户端的连接,没有这一步,accept() 调用会直接失败。
一句话 listen() 的核心意义就是明确告诉操作系统:这个服务器套接字已经完成了绑定(IP + 端口),现在正式 "就绪",可以开始接收客户端的连接请求了。
1. 标准原型
c
#include <sys/socket.h>
int listen(int sockfd, int backlog);
2. 参数详解
| 参数 | 含义与注意事项 |
|---|---|
sockfd |
输入参数,必须是已经通过 socket() 创建、且通过 bind() 绑定了地址的 TCP 套接字描述符(SOCK_STREAM 类型)。 |
backlog |
输入参数,核心是设置"未完成连接队列"+"已完成连接队列"的总长度上限(不同系统实现略有差异),具体见下文"工作原理"。 |
3. 返回值
- 成功:返回
0; - 失败:返回
-1,并设置全局变量errno表示错误原因(可通过perror()打印)。
4. 关键注意事项
listen()仅"开启监听",不会阻塞 :调用后函数立即返回,不会等待客户端连接(阻塞发生在accept()阶段);listen()不处理连接,只准备接收:真正取出连接的是accept(),listen()只是"搭好架子";- UDP 协议不支持
listen():UDP 是无连接协议,不需要监听,调用listen()会返回错误(EOPNOTSUPP)。
5. listen() 调用后的套接字状态变化
通过 netstat 命令可以直观看到变化:
- 调用
bind()后、listen()前:套接字状态为CLOSE或SYN_SENT(无监听); - 调用
listen()后:套接字状态变为LISTEN,表示已进入监听状态。
示例命令(查看 8080 端口):
bash
netstat -anp | grep 8080
# 输出示例(LISTEN 状态表示监听成功):
# tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 12345/./server
6. 总结
listen()的核心是将绑定后的 TCP 套接字转为监听状态,让内核为其维护连接队列,是服务器接收连接的前提;backlog参数主要控制已完成连接队列的长度,现代系统会自动调整,常用值 5/10/128 即可;- 关键特性:
listen()非阻塞、仅用于 TCP 服务器、调用后套接字状态变为LISTEN,且必须在bind()之后、accept()之前调用。
初始化完事儿后运行服务器。

修改运行状态,创建socket_in结构体 peer(输出型的参数,目的是带回申请连接的客户端)
调用 accept方法,这里会阻塞等待客户端申请连接。
关于accept
1.5.2 accept() 函数的核心作用
accept() 是专属于 TCP 服务器端 的系统调用,它的核心作用是:
从监听套接字(listen() 后的套接字)的"已完成连接队列"中,取出第一个完成三次握手的客户端连接,创建并返回一个全新的"通信套接字" ------这个新套接字专门用于和该客户端收发数据,而原监听套接字会继续保持 LISTEN 状态,等待其他客户端的连接。
简单来说,accept() 是服务器"接起电话"的动作:监听套接字是"总机"(一直等来电),accept() 是总机接线员,接起一个来电后,会分机到一个新的"通信套接字"(专门和这个客户通话),总机继续等下一个来电。
1. 标准原型
c
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
2. 参数详解
| 参数 | 类型 | 含义与使用注意事项 |
|---|---|---|
sockfd |
输入参数 | 必须是已调用 socket()→bind()→listen() 的监听套接字描述符(状态为 LISTEN)。 |
addr |
输出参数 | 指向 struct sockaddr 类型的指针,用于存储连接成功的客户端的 IP 地址和端口信息 ;若不关心客户端信息,可传 NULL。 |
addrlen |
输入/输出 | 指向 socklen_t 变量的指针: ① 调用前:赋值为 addr 结构体的长度(如 sizeof(struct sockaddr_in)); ② 调用后:内核会修改为实际存储的客户端地址长度。 若 addr 传 NULL,此参数也传 NULL。 |
3. 返回值
- 成功 :返回一个非负的新套接字描述符 (通信套接字,状态为
ESTABLISHED); - 失败 :返回
-1,并设置全局变量errno表示错误原因(可通过perror()打印)。
4. 关键特性:阻塞/非阻塞行为
accept() 的阻塞性由监听套接字的类型(阻塞/非阻塞)决定(默认是阻塞套接字):
阻塞模式(默认)
- 若"已完成连接队列"为空,
accept()会暂停程序执行(阻塞),直到队列中有新连接; - 这是最常用的模式,适合简单的单客户端服务器。
非阻塞模式
-
需先通过
fcntl()将监听套接字设为非阻塞:c#include <fcntl.h> // 将listen_fd设为非阻塞 int flags = fcntl(listen_fd, F_GETFL, 0); fcntl(listen_fd, F_SETFL, flags | O_NONBLOCK); -
若"已完成连接队列"为空,
accept()会立即返回 -1 ,并设置errno = EAGAIN或EWOULDBLOCK; -
适合高并发服务器(结合
select/epoll实现多路复用)。
5. 常见错误与注意事项
1. 高频错误原因
| errno 错误码 | 含义 | 常见场景 |
|---|---|---|
EINVAL |
参数无效 | 监听套接字未调用 listen() 就调用 accept(); |
EBADF |
文件描述符无效 | sockfd 不是合法的套接字描述符; |
EAGAIN/EWOULDBLOCK |
非阻塞模式下无连接 | 非阻塞 accept() 调用时,已完成队列为空; |
EINTR |
调用被信号中断 | 阻塞 accept() 时,程序收到信号(如 Ctrl+C); |
2. 重要注意事项
- 两个套接字的区分 :
accept()返回的conn_fd是通信套接字,原listen_fd是监听套接字,二者独立,关闭conn_fd不影响listen_fd; - 监听套接字状态不变 :
accept()成功后,listen_fd仍为LISTEN状态,可继续调用accept()接收新客户端; - 客户端信息的字节序 :
client_addr.sin_addr(IP)和client_addr.sin_port(端口)都是网络字节序 ,需用inet_ntop()/ntohs()转为主机字节序的字符串/数字。
6. 总结
accept()的核心是从已完成连接队列取出连接,创建新的通信套接字 ,原监听套接字保持LISTEN状态;- 参数中
addr/addrlen是输出参数,用于获取客户端信息,不关心可传NULL; - 阻塞性由监听套接字类型决定:默认阻塞(等连接),非阻塞模式下无连接会立即返回错误;
- 关键区分:三次握手由内核自动完成,
accept()仅负责"取出"已建立的连接,不参与握手过程。
emmm...我的理解就是:调用accept本质上就是创建了一个新的套接字并且新套接字的状态是已经连接,作用就是和客户端进行通信,并且原先监听套接字并不改变,监听套接字的作用就只是监听和维护已经建立连接的队列。所以这里也就能理解为啥服务端类的成员变量是监听套接字。
ok,这里理一下思路
首先更改服务器运行状态,创建peer结构体(作用是带回连接的套接字),算一下peer类型的大小(等下特定服务里面有用),创建新的套接字描述符(文件描述符),调用accept函数(参数含义:和那个监听套接字里的队列连接?强制类型转化一下带回的参数,长度),判断是否连接成功(如果没有则跳过本次连接),(连接成功)创建InetAddr对象(用于输出日志信息),输出日志信息(让服务器打印日志,知道和谁建立了连接),调用指定服务(参数:服务器和客户端套接字信息)。

调用到service。明确我们的目标是接收客户端传来的信息,然后再回显。
创建缓冲区,读取数据(这里用的就是read函数,因为是面向字节流的)。关于read函数的参数(从哪读,读到哪,读的大小)
接着判断是否读取成功。然后用日志格式化一下要输出的信息,最后用write函数输出(依旧是因为write函数是面向字节流的)参数的意义:写到哪,写什么,写多少。
这里服务端就设计完了,接着看客户端:

指定调用格式,调用的时候要说清楚服务器的IP和端口。

创建客户端的sockfd,判断是否创建成功。

创建InetAddr对象,方便做格式的转化。
调用connect方法
关于connect方法
1.5.3 connect() 函数的核心作用
connect() 是专属于 TCP 客户端 的系统调用,它的核心作用是:
让客户端套接字主动向服务器端的监听套接字(LISTEN 状态)发起 TCP 连接请求,触发并完成三次握手,最终建立客户端与服务器之间的双向通信链路。
简单来说,connect() 是客户端"主动打电话给服务器"的动作------服务器的监听套接字是"总机",客户端通过 connect() 拨打这个总机号码,完成三次握手后,服务器的 accept() 会"接起电话"并分配通信套接字,双方就能开始对话。
1. 标准原型
c
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2. 参数详解
| 参数 | 类型 | 含义与使用注意事项 |
|---|---|---|
sockfd |
输入参数 | 客户端通过 socket() 创建的 TCP 套接字描述符(SOCK_STREAM 类型),无需提前 bind()(内核会自动分配临时端口)。 |
addr |
输入参数 | 指向 struct sockaddr 类型的指针,存储服务器的 IP 地址和端口信息 (必须是服务器已绑定、且处于 LISTEN 状态的地址)。 |
addrlen |
输入参数 | addr 结构体的长度(如 sizeof(struct sockaddr_in))。 |
3. 返回值
- 成功 :返回
0,表示三次握手完成,客户端套接字进入ESTABLISHED(已建立)状态; - 失败 :返回
-1,并设置全局变量errno表示错误原因(可通过perror()打印)。
关键细节:
connect()的阻塞/返回时机:默认情况下,connect()会阻塞到三次握手完全完成(客户端发送 ACK 后)才返回 0;- 客户端无需提前
bind():调用socket()后可直接connect(),内核会自动为客户端分配本机 IP 和临时端口(如 54321); - 三次握手是内核自动完成的:
connect()只是触发这个流程,无需开发者干预。
4. 关键特性:阻塞/非阻塞行为
connect() 的阻塞性由客户端套接字的类型(阻塞/非阻塞)决定(默认是阻塞套接字):
阻塞模式(默认)
- 调用后阻塞,直到三次握手完成(返回 0)或超时/失败(返回 -1);
- 超时时间由内核决定(Linux 下约 75 秒),超时后
errno设为ETIMEDOUT; - 这是最常用的模式,适合简单客户端。
非阻塞模式
-
需先将客户端套接字设为非阻塞:
c#include <fcntl.h> int flags = fcntl(client_fd, F_GETFL, 0); fcntl(client_fd, F_SETFL, flags | O_NONBLOCK); -
调用
connect()后会立即返回 -1 ,但errno设为EINPROGRESS(表示三次握手正在进行中); -
需通过
select()/epoll()监听套接字的"可写事件",来判断三次握手是否完成; -
适合高并发客户端(如同时连接多个服务器)。
5. 常见错误与注意事项
1. 高频错误原因
| errno 错误码 | 含义 | 常见场景 |
|---|---|---|
ECONNREFUSED |
连接被拒绝 | 服务器未启动、端口错误,或服务器未调用 listen(); |
ETIMEDOUT |
连接超时 | 服务器不可达(如网络不通),或三次握手超时; |
EINPROGRESS |
连接正在进行中 | 非阻塞模式下 connect() 立即返回的正常状态; |
EADDRINUSE |
地址已被使用 | 客户端手动 bind() 了已占用的端口; |
EHOSTUNREACH |
主机不可达 | 服务器 IP 错误,或路由不通; |
2. 重要注意事项
- 客户端无需 bind() :除非需要固定客户端端口,否则直接
socket()→connect()即可,内核会自动分配临时端口; - 连接失败后需重新创建套接字 :若
connect()失败(如超时),该套接字无法再次用于connect(),需关闭后重新socket(); - UDP 不支持 connect()(但可调用) :UDP 是无连接协议,调用
connect()仅会记录服务器地址,不会建立连接,后续send()无需指定地址。
6. 总结
connect()是客户端主动发起 TCP 连接的核心接口,触发并等待三次握手完成,成功后客户端套接字进入ESTABLISHED状态;- 客户端套接字无需提前
bind(),内核会自动分配 IP 和临时端口; - 阻塞模式下
connect()等待三次握手完成,非阻塞模式需通过select()/epoll()监听连接状态; - 核心交互逻辑:客户端
connect()触发三次握手 → 服务器内核将连接放入 ACCEPT 队列 → 服务器accept()取出连接,双方开始通信。
理解 connect() 的关键是抓住"客户端主动发起三次握手"这个核心,它和服务器的 accept() 是一一对应的------connect() 是"拨打电话",accept() 是"接起电话"。

我的理解:调用connect方法(谁要连接?连接谁?连接服务器的长度?)
判断是否连接成功

执行向服务器写逻辑:创建字符转用于接受输入,使用getline函数接受来自标准输入的参数,房子line中。
调用write函数,写到客户端的文件描述符中,指定写的内容,写的大小。

回显逻辑:创建缓冲区,调用read函数(从哪读?读到哪?读多少?)
判断是否读取成功。将读取到的buffer中的内容输出到屏幕。最后关闭文件。
Q:有点迷的是为啥也从socket里面读?这里的socket不是客户端的套接字吗?
A:先回答问题:因为服务器写到了socket里面,socket是客户端的套接字。
其实整个代码的执行逻辑是:服务器先运行,创建了监听套接字,不断地监听,并且也已经调用了accept方法,服务器运行到这里是不断阻塞的,就等待客户端的连接。随后客户端建立连接(客户端已经创建好了套接字),然后服务器就接收到了客户端的连接,并且创建了一个套接字用来接受accept方法的返回值。这里创建的套接字是共给服务器使用的,随后调用服务(传参就是服务器的套接字和客户端的套接字)

**至此第一版的demo已经设计完毕。**第一版的缺陷是,只允许单进程访问服务器,这显然不符合我们的常理对么?所以第二版引入多进程。
demo1 v2
相较于第一版,第二版只需要改一下服务端的逻辑
server.hpp
cpp
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
{
}
void Init()
{
// 回收子进程做法1.
// signal(SIGCHLD, SIG_IGN); // 父进程忽略子进程信号,让内核帮着回收子进程
// 1. 创建套接字,socket类比open
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;
// 2. bind端口号
InetAddr local(_port);
int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bing error";
exit(BIND_ERR);
}
LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;
// 3. 设置socket状态
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
}
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设置成C风格的字符串
buffer[n] = 0;
LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
// 2. 写回数据
std::string echo_string = buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
close(sockfd);
break;
}
else
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
close(sockfd);
break;
}
}
}
void Run()
{
_isrunning = true;
while (_isrunning)
{
// 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
int sockfd = accept(_listensockfd, CONV(peer), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
InetAddr addr(peer);
LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();
// Service(sockfd, addr);
pid_t id = fork();
if(id < 0)
{
LOG(LogLevel::FATAL) << "fork error";
exit(FORK_ERR);
}
else if(id == 0) // 子进程
{
close(_listensockfd); // 关掉不想让子进程访问的套接字,关闭文件描述符
// 回收子进程方法2:
if(fork() > 0) // 子进程再次创建子进程,也就是让孙子进程去执行业务
exit(OK);
Service(sockfd, addr); //孙子进程处理业务,孙子进程变成了孙子进程,被1号进程领养,被系统回收
exit(OK);
}
else
{
close(sockfd); // 父进程管监听就行,不用管其他的套接字。
pid_t rid = waitpid(id, nullptr, 0); // 回收子进程,采用方法2已经不会再阻塞,因为子进程瞬间退出了
(void)rid;
}
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
};
解释

思想:在执行业务之前,创建子进程,让子进程去执行业务,父进程持续的监听客户端。如此就能实现多进程访问服务器。根本原因是:父子进程并发运行。

对于子进程来说,是不要和监听作用隔离的,也就是不想让子进程访问监听套接字,所以关闭对应的文件描述符。
然后去执行对应的业务。
这里的问题是父进程要等待子进程,我们知道,父进程等待子进程是阻塞等待,如果阻塞等待的话,就有变成了串行,还是解决不了多客户端访问的问题。
对于这个问题有两个解决办法:
- 忽略子进程的信号

- 创建孙子进程

对于方法二,注释已经写的很清楚,再次不多赘述。
到此就完成了demo的第二版,多进程访问服务器。
下面第三版是多线程访问服务器。
demo1 v3
server.hpp
cpp
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
const static int defaultsockfd = -1;
const static int backlog = 8;
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port) : _port(port), _listensockfd(defaultsockfd), _isrunning(false)
{
}
void Init()
{
// 1. 创建套接字,socket类比open
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel ::INFO) << "socket success: " << _listensockfd;
// 2. bind端口号
InetAddr local(_port);
int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bing error";
exit(BIND_ERR);
}
LOG(LogLevel ::INFO) << "bind success: " << _listensockfd;
// 3. 设置socket状态
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel ::INFO) << "listen success: " << _listensockfd;
}
class ThreadData
{
public:
ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
{
}
int sockfd;
InetAddr addr;
TcpServer *tsvr;
};
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设置成C风格的字符串
buffer[n] = 0;
LOG(LogLevel::DEBUG) << peer.StringAddr() << "say# " << buffer;
// 2. 写回数据
std::string echo_string = buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
close(sockfd);
break;
}
else
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
close(sockfd);
break;
}
}
}
static void *Routine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->tsvr->Service(td->sockfd, td->addr);
delete td;
return nullptr;
}
void Run()
{
_isrunning = true;
while (_isrunning)
{
// 获取链接 :其实内核已经建立连接了,获取链接得意义是让程序员知道服务器被谁链接,方便通信
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
int sockfd = accept(_listensockfd, CONV(peer), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
InetAddr addr(peer);
LOG(LogLevel ::INFO) << "accept success, peer addr: " << addr.StringAddr();
ThreadData *td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning;
};
demo2
该版本继续对上面升级,使用多线程执行具体的业务(远程执行命令行)。
完