Hello大家好,上一期我们把UDP套接字编程的细节拆了个遍。这一篇是 Linux 网络编程系列的第六篇,主角换成面向连接的TCP,重点拆解TCP套接字编程的基础流程与核心API。
文章会先从socket()创建套接字、bind()绑定地址讲起,把参数含义和初始化规范一条条说清楚;再拿UDP做参照,重点聊TCP独有的listen()、监听状态(LISTEN),以及怎么用 netstat 这类工具查看连接状态;最后用一个"鱼庄拉客"的生活类比分,把accept()的工作原理掰开揉碎,讲明白"监听套接字"和"服务套接字"到底怎么分工,顺便看看单进程TCP服务器的瓶颈卡在哪里。
好的,我们开始吧。
目录
[1.1 创建套接字:socket函数](#1.1 创建套接字:socket函数)
[1.2 绑定本地地址:bind函数](#1.2 绑定本地地址:bind函数)
[1.3 开启监听:listen函数与TCP监听状态](#1.3 开启监听:listen函数与TCP监听状态)
[1.3.1 listen函数详解:参数与监听队列](#1.3.1 listen函数详解:参数与监听队列)
[1.3.2 查看网络进程的监听状态](#1.3.2 查看网络进程的监听状态)
[1.3.3 TCP连接建立过程与状态转换](#1.3.3 TCP连接建立过程与状态转换)
[1.4 接受连接:accept函数与套接字的分工](#1.4 接受连接:accept函数与套接字的分工)
[1.4.1 从"好再来鱼庄"的故事理解监听与接待](#1.4.1 从“好再来鱼庄”的故事理解监听与接待)
[1.4.2 accept函数详解:获取已建立的连接](#1.4.2 accept函数详解:获取已建立的连接)
[1.4.3 连接建立后的数据读取与写入](#1.4.3 连接建立后的数据读取与写入)
[1.5 客户端如何连接服务器:connect函数](#1.5 客户端如何连接服务器:connect函数)
[1.5.1 connect函数的基本用法](#1.5.1 connect函数的基本用法)
[1.5.2 connect函数的使用示例](#1.5.2 connect函数的使用示例)
[1.5.3 connect函数背后的TCP连接建立过程](#1.5.3 connect函数背后的TCP连接建立过程)
[1.5.4 connect、listen与accept之间的关系](#1.5.4 connect、listen与accept之间的关系)
[1.5.5 客户端需要提前调用bind吗?](#1.5.5 客户端需要提前调用bind吗?)
[2.1 InetAddr.hpp(IP 地址封装类)](#2.1 InetAddr.hpp(IP 地址封装类))
[2.2 TcpServer.hpp(TCP 服务器类)](#2.2 TcpServer.hpp(TCP 服务器类))
[2.3 TcpServer.cc(服务端主程序)](#2.3 TcpServer.cc(服务端主程序))
[2.4 TcpClient.cc(客户端主程序)](#2.4 TcpClient.cc(客户端主程序))
[2.5 Makefile](#2.5 Makefile)
[2.6 运行方式](#2.6 运行方式)
一、TCP套接字API详解:从创建套接字到建立连接
搞TCP网络编程,第一件事得把Linux提供的标准套接字接口摸熟。这些接口都定义在<sys/socket.h>头文件里。跟UDP那边一样,TCP通信的第一步也是先创建一个套接字,把通信端点立起来。
1.1 创建套接字:socket函数
socket()函数干的事,就是打开一个网络通信端口。调用成功的话,它跟open()系统调用一个套路,返回一个套接字文件描述符(Socket Descriptor)。后面所有的网络读写操作,都围着这个描述符转。函数声明:
cpp
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数与返回值,跟UDP基本一个模子。唯一的区别在type:指定数据传输的服务类型。TCP这边,type必须写成SOCK_STREAM,表示面向连接的、可靠的字节流传输协议。
1.2 绑定本地地址:bind函数
套接字创建出来了,还只是个空壳子,没跟任何地址挂上钩。bind()的活,就是把这个套接字跟本地的IP地址和端口号绑在一起。函数声明:
cpp
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
三个参数,逐个拆:
- sockfd:socket()返回的那个套接字文件描述符,告诉函数要绑哪个套接字。
- addr:指向本地地址结构体的指针。实际填的是struct sockaddr_in,传进去的时候强转成通用的struct sockaddr*。
- addrlen:地址结构体的大小,传sizeof(struct sockaddr_in) 就行。
返回值:成功返回 0,失败返回 -1,并设置 errno。
1.3 开启监听:listen函数与TCP监听状态
UDP是无连接的,发出去就不管了,压根没有"等谁来连"这回事。TCP不一样,它是面向连接的,连接没建立起来,数据就没法传。
打个餐馆的比方。你去吃饭,老板一定守在店里,随时等着客人上门。客人没来,他也守着;客人来了一拨又一拨,他继续守着。这种"一直等在门口"的姿态,就是监听。
邮件是另一回事。你发一封邮件,收件人不需要坐在电脑前一直等,邮件先躺在服务器上,他什么时候登录什么时候取。发件人和收件人,根本不用同时在线。这就是面向数据报的路子。
TCP走的是餐馆模式:服务端得先摆好架势,进入监听状态,等着客户端来连。连接没来,它就守着;连接来了,它才起身接客。所以listen()这个函数,UDP那边压根用不上,TCP这边却绕不开。
1.3.1 listen函数详解:参数与监听队列
listen()干的事,一句话:让sockfd进入监听状态,摆好架势,等着客户端上门。
门口不可能只来一个人。多个客户端同时发起连接请求,这些请求就得排队等着处理。listen()的第二个参数,管的就是这条队最多能排多长。函数签名:
cpp
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数与返回值,逐个拆:
- sockfd:已经用bind()绑好地址和端口号的套接字文件描述符。地址都没绑,监听个啥?所以bind一定得在listen前面。
- backlog:提示队列的最大排队连接数。说白了,就是"门口最多允许多少人排队等"。最多允许backlog个客户端处于连接等待状态,超出的连接请求,系统直接忽略,连排队的机会都不给。这个值通常不会设得太大,5、128都是常见的选择。设太大,队列冗长,处理不过来反而添乱;设太小,又容易把正常请求挡在门外。
返回值:成功返回0,失败返回-1。
1.3.2 查看网络进程的监听状态
只要TCP服务器成功调用了listen(),进入LISTEN状态,它就已经具备了接受客户端连接的能力,门开了,就等人来。想看系统里哪些套接字正在监听,Linux命令行工具就能搞定:
bash
# 查看所有 TCP 和 UDP 监听及连接状态(显示 PID 及程序名)
netstat -tnap
# 仅查看处于 LISTEN 状态的套接字
netstat -tnlp
常用参数,逐个认一下:
-
-t**(tcp)**:只看TCP相关的套接字。
-
-u**(udp)**:只看UDP相关的套接字。
-
-n(numeric):直接显示数字形式的IP和端口,不做域名和服务名解析。速度快,看着也清爽。
-
-l**(listening)**:只揪出处于LISTEN状态的套接字。想看谁在守店,加它就对了。
-
-p**(programs)**:显示进程名称,顺带把PID也亮出来。谁在监听,一查便知。
1.3.3 TCP连接建立过程与状态转换
拿telnet工具向服务器发起连接,整个状态变化过程一目了然。
未连接之前,服务器进程就守在LISTEN状态,安安静静等客上门:
bash
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1080682/./tcpserver
客户端执行telnet 127.0.0.1 8080建立连接之后,网络状态里立刻多出对应的 ESTABLISHED 记录,表示连接已经成功建立:
bash
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1080682/./tcpserver
tcp 0 0 127.0.0.1:58752 127.0.0.1:8080 ESTABLISHED 1080746/telnet
tcp 0 0 127.0.0.1:8080 127.0.0.1:58752 ESTABLISHED -
仔细看,同一台主机上测试的时候,netstat输出里冒出了两条ESTABLISHED记录。为什么?
第一,TCP是全双工通信。 通信双方既是发送方,也是接收方,两条方向相反的数据通道各自独立存在。一条连接,两头各有一条链路,系统两边都得记着。
**第二,同机测试。**客户端和服务器部署在同一台机器上,系统在回环网卡(127.0.0.1)上,同时记录了"客户端端口 → 服务器端口"和"服务器端口 → 客户端端口"这两条对称链路。所以看起来像是两条连接,实际上是同一条TCP连接的两端,各自记了一笔。
1.4 接受连接:accept函数与套接字的分工
学TCP套接字,最容易让人犯迷糊的,莫过于accept()的返回值,为什么调用它之后,系统又甩给你一个全新的套接字文件描述符?原来创建的那个套接字,又跑哪儿去了?
1.4.1 从"好再来鱼庄"的故事理解监听与接待
假设西湖边开着一家极其火爆的"好再来鱼庄"。

拉客员(张三):站在鱼庄门口、马路边,专门负责热情招呼路过的游客。他的工作非常纯粹,只要看到有顾客想用餐,就把人引导进店,交给店内的服务员。点菜、端茶、倒水,一概不归他管。引导完一位客人,他立刻回到门口,继续拉客。
服务员(李四、王二麻):坐在店内,专门对接已经被张三带进来的顾客。点菜、上菜、陪聊、结账,具体的服务工作全由他们负责。
这个故事里,两个角色对应得清清楚楚:
-
**监听套接字(listensockfd)就是拉客员张三。**它唯一的使命,是在特定端口上监听外部请求,负责建立并"获取"连接,不承担后续与客户端的数据通信(I/O)。拉完客,它扭头就走,回门口继续守着。
-
**服务套接字(accept()返回的sockfd)就是服务员李四。**连接建立之后,系统专门分配这个套接字,来跟该具体客户端进行read/write数据交互。接待、服务、收尾,全是它的活儿。
1.4.2 accept函数详解:获取已建立的连接
这里有个认知必须先摆正:TCP三次握手建立连接的过程,完全由操作系统内核自动完成,跟有没有调用accept()没有半点关系。 连接在accept()之前就已经建好了,安安静静躺在内核的已完成连接队列里。accept()的本质作用,是从这个队列里取走一个已经建立好的连接。
函数签名:
cpp
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数与返回值,逐个拆:
-
sockfd:监听套接字描述符,也就是listensockfd,对应拉客员张三。它专门传进accept()用于获取连接。
-
addr:输出型参数,用来获取连接成功的客户端网络地址信息,包含客户端的IP和端口号。谁来了,一查便知。
-
addrlen:值-结果参数。传进去时,是addr结构体的长度;函数返回时,变成实际写入的地址结构体大小。
-
返回值 :成功时,返回一个全新的套接字文件描述符,服务套接字,对应店里的服务员。获取失败,返回-1。
后续针对该客户端的所有数据传输,read/write,或者recv/send,统统使用accept()返回的这个新套接字来操作。拉客员只负责把人带进来,端茶倒水、点菜结账,是服务员的活儿。
1.4.3 连接建立后的数据读取与写入
收发数据这一块,TCP跟UDP走的是两条路。
UDP面向数据报,有自己专门的收发接口recvfrom/sendto,每个数据包独立处理,边界清清楚楚。
TCP面向字节流,没有"包"的概念,数据就是一条连续的水流。所以它不需要什么专用接口,直接用标准的Linux文件I/O系统调用read()和write()就行。套接字在Linux里本来就是个文件描述符,读写它,跟读写普通文件是一个道理。水流进来,水流出去,字节连着字节,边界得由应用层自己去划。
1.5 客户端如何连接服务器:connect函数
前面我们已经介绍了TCP服务器端的基本流程:通过socket()创建套接字,使用bind()绑定本地地址,再通过listen()进入监听状态,最后调用accept()获取客户端连接。
那么问题来了:服务器已经准备好了,客户端应该如何主动发起连接呢?
答案就是connect() 函数。connect()是TCP客户端编程中的核心接口,它的主要作用是主动向指定的服务器地址发起连接请求。当连接建立成功后,客户端就可以通过这个套接字进行数据的发送与接收。
1.5.1 connect函数的基本用法
首先来看connect()的函数原型:
cpp
#include <sys/socket.h>
int connect(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
这个函数有三个参数:
-
sockfd: 由 socket() 创建的套接字文件描述符,表示客户端准备使用哪个套接字建立连接。
-
addr: 指向服务器地址结构体的指针,用于指定要连接的目标IP地址和端口号。
-
addrlen: 地址结构体的长度,通常通过sizeof()获取。
返回值也比较简单:
-
返回0: 连接建立成功。
-
返回-1: 连接失败,同时设置errno,可以通过perror()等方式查看错误信息。
1.5.2 connect函数的使用示例
在调用connect()之前,我们需要先创建TCP套接字,并准备好服务器的地址信息。
例如,假设服务器监听的IP地址为127.0.0.1,端口号为8080,客户端可以这样编写:
cpp
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstdio>
int main()
{
// 1. 创建 TCP 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("socket");
return 1;
}
// 2. 填充服务器地址
struct sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
if (inet_pton(AF_INET, "127.0.0.1",
&server_addr.sin_addr) != 1)
{
fprintf(stderr, "IP地址格式无效\n");
close(sockfd);
return 1;
}
// 3. 主动发起连接
if (connect(sockfd,
(struct sockaddr*)&server_addr,
sizeof(server_addr)) == -1)
{
perror("connect");
close(sockfd);
return 1;
}
printf("连接服务器成功!\n");
// 4. 后续可以通过sockfd进行数据收发
// send(sockfd, ...);
// recv(sockfd, ...);
close(sockfd);
return 0;
}
这段代码的执行过程可以分成四步:
-
调用socket()创建一个TCP套接字。
-
填写服务器的IP地址与端口号,并完成必要的网络字节序转换。
-
调用connect(),主动请求与服务器建立连接。
-
连接成功后,使用同一个sockfd进行后续数据收发。
1.5.3 connect函数背后的TCP连接建立过程
调用connect()并不意味着客户端只是简单地记录一个服务器地址。对于TCP而言,它会触发真正的连接建立过程。
在正常的TCP三次握手过程中,通信双方需要完成以下步骤:
-
第一次握手: 客户端向服务器发送SYN,请求建立连接。
-
第二次握手: 服务器收到请求后,返回SYN + ACK,表示确认客户端的请求,同时也请求建立连接。
-
第三次握手: 客户端收到响应后,再发送ACK,双方完成连接建立。
当连接成功建立后,客户端的connect()返回0,后续便可以通过该套接字进行TCP数据通信。
此时,服务端的accept()负责从已完成连接的队列中取出连接,并返回一个新的套接字文件描述符,供服务端与该客户端进行通信。
这里有一个容易混淆的细节:客户端的connect()成功返回,并不意味着服务端应用程序已经执行了accept()。 TCP握手可以先由内核完成,已建立的连接随后等待服务端程序调用 accept() 取出。
1.5.4 connect、listen与accept之间的关系
将客户端和服务端的流程放在一起,就更容易理解这三个接口的分工。
| 接口 | 所在一侧 | 核心作用 |
|---|---|---|
| connect() | 客户端 | 主动发起连接请求 |
| listen() | 服务端 | 将TCP套接字置于监听状态,准备接收连接 |
| accept() | 服务端 | 获取一个已建立的连接,并返回用于通信的新套接字 |
可以将这个过程概括为:
客户端调用connect()发起连接,服务端通过listen()等待连接,并调用accept()获取连接。
还要注意,服务端的监听套接字与accept()返回的已连接套接字并不是同一个套接字。监听套接字继续负责接收后续连接,而新返回的套接字负责与某个客户端进行数据通信。
因此,服务器可以在保留监听能力的同时,与已经连接的客户端交换数据。
1.5.5 客户端需要提前调用bind吗?
前面介绍服务器端时,我们专门讲过bind()函数。那么客户端调用connect()之前,是否也必须通过bind()指定自己的本地IP地址和端口号呢?
通常不需要。
如果客户端没有显式调用bind(),操作系统一般会在连接过程中自动选择合适的本地IP地址和一个可用的临时端口号。客户端只需要指定目标服务器的地址,就可以发起连接。
当然,如果业务确实要求客户端使用指定的本地地址或端口,也可以提前调用bind()。
这样就能理解,服务器通常需要显式绑定固定端口,以便客户端能够按照约定访问;而普通客户端一般不需要固定本地端口,因为操作系统可以自动分配。
最后还需要区分阻塞与非阻塞模式:对于默认的阻塞套接字,connect()通常会等待连接建立成功或失败;对于非阻塞套接字,如果连接暂时无法立即完成,函数通常会返回-1,并将errno设置为 EINPROGRESS,此时需要通过poll()、select()等机制继续判断连接结果。
二、完整源码与运行测试
下面把TCP服务器与客户端的完整代码整理出来,包括IP地址封装类、TCP服务器类、服务端主程序、客户端主程序,以及Makefile。代码可直接编译运行。
2.1 InetAddr.hpp(IP 地址封装类)
cpp
#pragma once
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <string>
#include <cstring>
namespace NetModule
{
class InetAddr
{
public:
// 从 sockaddr_in 结构提取 IP 和端口(服务端获取客户端地址时使用)
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
_port = ntohs(_addr.sin_port);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
// 从 IP 和 Port 构建 sockaddr_in 结构(客户端指定服务端地址时使用)
InetAddr(const std::string &ip, uint16_t port)
: _ip(ip), _port(port)
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
}
std::string Ip() const { return _ip; }
uint16_t Port() const { return _port; }
struct sockaddr_in GetAddr() const { return _addr; }
~InetAddr() {}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
}
2.2 TcpServer.hpp(TCP 服务器类)
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "InetAddr.hpp"
namespace NetModule
{
// 业务处理函数类型:输入请求字符串,返回响应字符串
using func_t = std::function<std::string(const std::string &)>;
const uint16_t DEFAULT_PORT = 8080;
const int BACKLOG = 5;
class TcpServer
{
public:
TcpServer(uint16_t port = DEFAULT_PORT, func_t func = nullptr)
: _port(port), _func(func), _isrunning(false), _listensockfd(-1)
{
}
// 初始化:创建套接字 + 绑定地址 + 进入监听状态
void Init()
{
// 1. 创建 TCP 套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(1);
}
std::cout << "socket create success, listensockfd: " << _listensockfd << std::endl;
// 2. 设置端口复用,避免服务器重启时 bind 失败
int opt = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 填充本地地址结构体
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// 4. 绑定地址
int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
std::cerr << "bind error" << std::endl;
exit(2);
}
std::cout << "bind success, port: " << _port << std::endl;
// 5. 进入监听状态
n = listen(_listensockfd, BACKLOG);
if (n < 0)
{
std::cerr << "listen error" << std::endl;
exit(3);
}
std::cout << "listen success" << std::endl;
}
// 启动服务器:循环 accept + 读写
void Start()
{
_isrunning = true;
// 忽略 SIGCHLD,避免子进程变成僵尸
signal(SIGCHLD, SIG_IGN);
while (_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 获取一个已建立好的连接
int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
InetAddr peerAddr(peer);
std::cout << "[" << peerAddr.Ip() << ":" << peerAddr.Port()
<< "]# 连接建立, 服务套接字: " << sockfd << std::endl;
// 2. 创建子进程处理该客户端
pid_t id = fork();
if (id == 0)
{
close(_listensockfd); // 子进程不需要监听套接字
Service(sockfd, peerAddr);
close(sockfd);
exit(0);
}
close(sockfd); // 父进程不需要服务套接字
}
}
~TcpServer()
{
if (_listensockfd >= 0)
{
close(_listensockfd);
}
}
private:
// 与单个客户端通信
void Service(int sockfd, const InetAddr &peerAddr)
{
char buffer[1024];
while (true)
{
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::string request = buffer;
std::cout << "[" << peerAddr.Ip() << ":" << peerAddr.Port()
<< "]# " << request << std::endl;
std::string response;
if (_func)
{
response = _func(request);
}
else
{
response = "Server received: " + request;
}
write(sockfd, response.c_str(), response.size());
}
else if (s == 0)
{
std::cout << "[" << peerAddr.Ip() << ":" << peerAddr.Port()
<< "]# 客户端断开连接" << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
int _listensockfd;
uint16_t _port;
bool _isrunning;
func_t _func;
};
}
2.3 TcpServer.cc(服务端主程序)
cpp
#include "TcpServer.hpp"
#include <memory>
// 业务处理回调:把收到的字符串转成大写再返回
std::string Handler(const std::string &request)
{
std::string response = request;
for (auto &ch : response)
{
if (ch >= 'a' && ch <= 'z')
{
ch -= 32;
}
}
return response;
}
// 用法:./tcp_server [端口号]
int main(int argc, char *argv[])
{
uint16_t port = NetModule::DEFAULT_PORT;
if (argc == 2)
{
port = static_cast<uint16_t>(std::stoi(argv[1]));
}
std::unique_ptr<NetModule::TcpServer> server =
std::make_unique<NetModule::TcpServer>(port, Handler);
server->Init();
server->Start();
return 0;
}
2.4 TcpClient.cc(客户端主程序)
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 用法:./tcp_client [服务端IP] [服务端端口]
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = static_cast<uint16_t>(std::stoi(argv[2]));
// 1. 创建 TCP 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
// 2. 填充服务端地址结构体
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
// 3. 向服务端发起连接(客户端不需要 bind)
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect error" << std::endl;
close(sockfd);
return 3;
}
std::cout << "connect success" << std::endl;
// 4. 循环发送 + 接收
while (true)
{
std::string line;
std::cout << "Please Enter# ";
std::getline(std::cin, line);
if (line == "quit" || line == "exit")
{
break;
}
write(sockfd, line.c_str(), line.size());
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << "Server Response# " << buffer << std::endl;
}
else if (s == 0)
{
std::cout << "服务端已关闭连接" << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
close(sockfd);
return 0;
}
2.5 Makefile
cpp
.PHONY: all
all: tcp_server tcp_client
tcp_server: TcpServer.cc TcpServer.hpp InetAddr.hpp
g++ -o $@ $< -std=c++17
tcp_client: TcpClient.cc
g++ -o $@ $< -std=c++17
.PHONY: clean
clean:
rm -f tcp_server tcp_client
2.6 运行方式
编译:
make
先启动服务端(默认端口8080):
./tcp_server
再启动客户端,指定服务端IP和端口:
./tcp_client 127.0.0.1 8080
客户端输入内容,服务端会收到并打印,同时把字符串转成大写返回给客户端。输入quit或exit退出客户端。
如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连支持。你的每一次反馈,都是我继续硬核输出的最大动力。我们下篇见。