Linux 网络编程套接字
Linux 网络编程套接字
13.1 背景概念
13.1.1 源IP地址与目标IP地址理解
在IP网络通信中,IP数据包是实现主机间通信的基本单元。在每一个IP数据包的首部(Header)中,包含两个关键字段:源IP地址(Source IP Address) 和 目标IP地址(Destination IP Address),它们共同标识了数据包的发送方和接收方。
源IP地址指的是数据包最初发送方的IP地址,即该数据包是从哪个主机发出的。目标IP地址指的是数据包的接收方IP地址,即该数据包希望最终到达的目的主机地址。
这两个地址在整个数据传输过程中起着定位和路由的核心作用。路由器在转发数据包时,会依据目标IP地址决定下一跳的转发路径,确保数据包能够准确送达目标主机。而当目标主机处理完请求并回传响应时,会构造新的数据包,其中源IP和目标IP地址将与原始请求包的对应字段互换。
13.1.2端口号理解
IP地址+定位到主机,端口号定位到主机里的程序
举例:
打开微信和网页浏览器,它们都需要网络,但你电脑的 IP 地址是一样的,那数据怎么知道该给谁?
靠的就是端口号。微信用了端口号比如:5200;浏览器用了端口号比如:8080;数据包中除了有 IP 地址,还有端口号,确保:
收到的是网页的就发给浏览器,收到的是消息的就发给微信。
在 TCP/IP 协议中,端口号(Port Number) 是一个 16 位的无符号整数,范围为 0 ~ 65535。用于标识某台主机中运行的具体网络应用程序(或者进程)。
每个网络连接由以下四个元素唯一确定,称为四元组:
源IP地址、源端口号、目标IP地址、目标端口号
端口号的分类:
端口范围 | 类型 | 用途举例 |
0 ~ 1023 | 知名端口 | 如 HTTP 是 80,HTTPS 是 443,FTP 是 21 等 |
1024 ~ 49151 | 注册端口 | 用于注册服务,如数据库 MySQL 默认端口 3306 |
49152 ~ 65535 | 动态端口 / 私有端口 | 临时通信时操作系统随机分配 |
示例:
你访问一个网站:浏览器本地使用一个随机端口(比如:52634);请求发往服务器的 IP地址 + 端口80(HTTP服务);数据返回时服务器发给你:目标IP = 你,目标端口 = 52634。
13.1.3 TCP协议
TCP 是传输层协议,位于 IP 协议之上,它的主要作用是:实现两台主机之间可靠、有序的数据通信。
如果 IP 只是把“数据包”从 A 送到 B,TCP 就保证:
数据完整、不丢包;顺序正确;收到就一定会确认;不会重复、不乱序。
TCP 的核心特点详解:
传输层协议:TCP 处在 OSI 七层模型中的第四层(传输层)。它不关心数据的内容,也不管数据怎么显示,只负责把数据可靠、安全地送到对方程序。TCP 通过使用端口号,和应用层通信。
面向连接:
TCP 是面向连接的协议,发送数据前必须先建立连接(就像打电话前要先拨号)。建立连接过程:三次握手(Three-Way Handshake);断开连接过程:四次挥手(Four-Way Handshake);建立连接后,两台主机会知道彼此的状态,并做好收发数据的准备。
可靠传输:
TCP 使用多种机制来确保数据传输可靠
机制 | 作用说明 |
确认应答 ACK | 每收到一段数据就要回复确认(ACK) |
超时重传 | 如果没收到 ACK,就重新发送那段数据 |
序号(Sequence) | 数据有编号,保证顺序正确 |
滑动窗口 | 控制数据流速,避免网络拥堵 |
拥塞控制 | 根据网络状况自动调整发送速度 |
面向字节流:
TCP 把数据看成一个无结构的字节流,应用程序发送的数据被当作字节序列,TCP 会将它划分为合适的数据段进行传输。
举例来说,如果你用 TCP 发送了“hello world”,TCP 不会按“单词”或“句子”去看,而是:
h e l l o w o r l d |
然后将它拆成多个数据包发送,对方收到后再按顺序拼接
举例:
想象你给朋友邮寄一份书稿:你先打电话确认他在家(建立连接);把稿子拆成一页页放进信封编号(序号);每寄一封就等他回信告诉你“收到了”(ACK);如果过一阵没收到回信,就重寄(超时重传);他收到后按编号顺序整理成完整书稿(有序)。
13.1.4 UDP协议
UDP(用户数据报协议)是一个轻量级的传输层协议,它和 TCP 一样,也建立在 IP 协议之上。但与 TCP 不同的是:UDP 提供的是无连接、不可靠、面向数据报的传输服务;它牺牲了一些可靠性,换来了更高的效率和更少的延迟。
UDP 的核心特性详解:
传输层协议
UDP 也属于 传输层协议,与 TCP 处于同一层;它的主要作用是:为应用层提供简单、快速的数据传输服务;UDP 使用端口号来标识应用程序(同 TCP 一样)。
无连接
UDP 在发送数据前不需要建立连接,直接发送;就像发短信,写好就发,不管对方有没有准备好接收;优点是:速度快,开销小;缺点是:不能保证对方收到,也不知道对方状态
不可靠传输
UDP 不保证数据一定送达,也不确认是否送达,它只管发。
结果是:
速度快;可能丢包、乱序、重复;但有些应用可以容忍这些问题,比如:
视频会议(丢几帧没关系)、语音通话(不卡顿比准确更重要)、实时游戏(延迟更关键)
面向数据报
UDP 把应用层传来的数据称为数据报(Datagram),直接打包发送,每个数据报都是一个完整的独立单位。不像 TCP 把数据当作连续的字节流,UDP 一次发一个“整体”。
13.1.5 网络字节序
网络字节序是网络编程中必须理解的概念,它和计算机本地的大小端存储密切相关,是数据在不同主机之间准确传递的保障机制。
什么是字节序?
字节序就是多字节数据在内存中如何排列。
例如:0x12345678 是一个 4 字节整数(32 位),用十六进制表示。
大端字节序(Big Endian):
高位字节排在前(低地址)
存储顺序为:
地址:0x00 0x01 0x02 0x03 数据:0x12 0x34 0x56 0x78 |
小端字节序(Little Endian):
低位字节排在前(低地址)
存储顺序为:
地址:0x00 0x01 0x02 0x03 数据:0x78 0x56 0x34 0x12 |
注:
高字节(high byte):数值上更“靠左”的那个字节,比如上面的 0x12
低字节(low byte):数值上更“靠右”的那个字节,比如上面的 0x78
不同的 CPU 架构有不同的字节序:
x86(Intel/AMD)是小端
有些嵌入式系统(如 ARM)支持大/小端可切换
网络字节序
网络字节序就是大端字节序,即:高位字节先发送,低位字节后发送
这是 TCP/IP 协议强制规定的统一标准,确保不同主机之间可以准确地理解彼此传输的数值。
网络字节序就是大端字节序,小端机也会在“接收时”先把收到的大端序数据转回自己习惯的小端序,这依靠两个经典函数来完成。
#include<arpa/inet.h> // 主机序转网络序 uint32_t htonl(uint32_t hostlong); // //将一个 32 位的主机字节序整数转换为网络字节序。 //参数:hostlong 是一个 4 字节整数(如 IP 地址 192.168.1.1) //返回值:转换为大端的 4 字节整数 uint16_t htons(uint16_t hostshort); //将一个 16 位主机字节序整数转换为网络字节序。 //参数:hostshort 是一个 2 字节整数(如端口号) //返回值:转换为大端的 2 字节整数 // 网络序转主机序 uint32_t ntohl(uint32_t netlong); //将一个 32 位的网络字节序整数转换为主机字节序。 //参数:netlong 是一个网络字节序的整数 //返回值:转换为本机的字节序(如果你是小端机,就变成小端) uint16_t ntohs(uint16_t netshort); //将一个 16 位的网络字节序整数转换为主机字节序。 //参数:netshort 是一个网络字节序的短整数 //返回值:转换为主机字节序 16位(short) 是两个字节的数据 32位(long) 是四个字节的数据 |
13.2 socket编程接口
Socket(套接字)就是网络编程里的“插口”,让你的程序可以通过网络发送或接收数据, 在编程层面,Socket 是操作系统提供的网络通信接口。
Socket 编程的两种角色:
客户端(Client):主动发起连接
服务器(Server):被动等待连接
Socket 可以用在不同的协议上:
TCP Socket:可靠的、面向连接的(类似打电话)
UDP Socket:不可靠的、无连接的(类似发短信)
13.2.1 socket 常见API
int socket(int domain, int type, int protocol) (TCP/UDP, 客户端 + 服务器)
作用:创建一个套接字,返回套接字文件描述符
参数说明:
domain:协议族(地址族)
AF_INET:IPv4
AF_INET6:IPv6
AF_UNIX:本地进程间通信
type:套接字类型
SOCK_STREAM:面向连接的(TCP)
SOCK_DGRAM:无连接的(UDP)
protocol:协议,一般填 0(让系统自动选 TCP 或 UDP)
返回值:
成功:返回一个非负整数(socket 文件描述符)
失败:返回 -1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(TCP/UDP, 服务器)
作用: 将套接字与 IP 地址和端口号绑定。
参数说明:
sockfd:由 socket() 创建的套接字
addr:指向 sockaddr_in 结构体的指针(需强转)
addrlen:地址结构体大小(sizeof(struct sockaddr_in))
返回值:
成功:0
失败:-1
int listen(int sockfd, int backlog); (TCP, 服务器)
作用: 把套接字设为被动监听状态,准备接受连接。
参数说明:
sockfd:由 socket() 创建的套接字
backlog:连接队列的最大长度(一般设成 5 或更大)
返回值:
成功:0
失败:-1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); (TCP, 服务器)
作用: 接收连接请求,返回一个新的 socket 用于通信。
参数说明:
sockfd:监听套接字(由 listen() 设置)
addr:用于接收客户端地址信息
addrlen:传入传出参数,指定 addr 的长度
返回值:
成功:新的 socket 文件描述符(用于与客户端通信)
失败:-1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(TCP, 客户端)
作用: 客户端向服务端发起连接请求。
参数说明:
sockfd:由 socket() 创建的套接字
addr:服务端地址信息(sockaddr_in 强转)
addrlen:地址长度
返回值:
成功:0
失败:-1
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
( TCP, 客户端和服务器端)
作用: 发送数据
参数说明:
sockfd:通信 socket(不是监听 socket)
buf:要发送的数据指针
len:数据长度
flags:通常设为 0
返回值:
成功:返回实际发送的字节数
失败:-1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
( TCP, 客户端和服务器端)
作用: 接收数据,对应send
参数说明:
sockfd:通信 socket
buf:接收缓冲区
len:缓冲区大小
flags:通常设为 0
返回值:
成功:接收到的字节数,0 表示连接关闭
失败:-1
int close(int sockfd);
(TCP/UDP, 客户端 + 服务器)
作用: 关闭 socket 连接,释放资源。
参数说明:
sockfd:要关闭的套接字描述符
返回值:
成功:0
失败:-1
ssize_t recvfrom(int sockfd, void *buf, size_t len,int flags, struct sockaddr *src_addr, socklen_t *addrlen);(UDP, 接收数据,还能获取对方的 IP 和端口信息)
参数
sockfd 用 socket() 创建的 socket 文件描述符
buf 接收数据的缓冲区
len buf 的大小
flags 一般用 0(可选其他:MSG_PEEK, MSG_DONTWAIT 等)
src_addr 用于保存 发送方地址信息(可以是 sockaddr_in 强转过来的)
addrlen 传入传出参数,表示地址结构体大小
返回值
正常:返回接收到的字节数
失败:返回 -1,并设置 errno
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
sockfd 用 socket() 创建的 socket 文件描述符
buf 要发送的数据(指针)
len 要发送数据的长度
flags 设置为 0 通常就够了(可选 MSG_CONFIRM 等)
dest_addr 指定目标主机的地址(sockaddr_in 类型强转)
addrlen dest_addr 的大小,一般是 sizeof(sockaddr_in)
ssize_t read(int fd, void *buf, size_t count);
参数
fd 文件描述符,通常是 socket 返回的值
buf 缓冲区,用于存放读到的数据
count 最多读多少字节
返回值 实际读取的字节数(0 表示连接关闭,-1 表示出错)
ssize_t write(int fd, const void *buf, size_t count);
参数
fd 文件描述符,通常是 socket
buf 要发送的数据内容
count 要发送的数据字节数
返回值 实际写出的字节数(-1 表示出错)
Socket 编程中,TCP 和 UDP 使用的是同一套 API 接口函数,但根据协议的不同,使用方式略有区别
功能 | TCP(面向连接) | UDP(无连接) |
创建套接字 | socket(AF_INET, SOCK_STREAM, 0) | socket(AF_INET, SOCK_DGRAM, 0) |
绑定地址 | bind() | bind()(服务器使用) |
建立连接 | connect()(客户端) | 不需要(可选) |
监听连接 | listen()(服务器) | 不需要 |
接受连接 | accept()(服务器) | 不需要 |
发送数据 | send() / write() | sendto() or send() |
接收数据 | recv() / read() | recvfrom() or recv() |
主动关闭连接 | close() | close() |
UDP 的 connect() 只是指定默认目标地址,省得每次写 sendto()。但不建立真正连接。
TCP API 使用流程(面向连接)
适合:数据量大、可靠性高的通信,如网页访问、文件传输
客户端: socket(); connect(); send()/recv(); close(); 服务器端 socket(); bind(); listen(); accept(); send()/recv(); close(); |
UDP API 使用流程(无连接):
适合:快速、简单、低延迟通信,如 DNS、视频通话
客户端 socket(); sendto(); // 发送数据到某个IP:端口 recvfrom(); // 接收对方数据 close(); 服务器端 socket(); bind(); recvfrom(); sendto(); close(); |
13.2.2 sockaddr结构
struct sockaddr 是 Socket API 中用于不同协议族(IPv4、IPv6、UNIX 域套接字等)地址信息的统一表示形式,sockaddr 是所有 socket 地址结构体的通用表示形式。
struct sockaddr 是一个通用结构体,用于函数参数中,比如 bind()、connect()、accept() 要求的是:
const struct sockaddr *addr
但实际上传入的是更具体的结构体,比如:
struct sockaddr_in(用于 IPv4)
struct sockaddr_in6(用于 IPv6)
struct sockaddr_un(用于本地通信)
所以我们要强制类型转换:
(struct sockaddr *)&addr_in
sockaddr 定义如下(来自 <sys/socket.h>)
struct sockaddr { sa_family_t sa_family; // 地址族(如 AF_INET) char sa_data[14]; // 地址数据(具体内容依赖实际类型) }; |
实际开发中我们几乎不用它来存数据,而是用专门的结构体,例如:
IPv4 专用结构体:struct sockaddr_in
#include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; // 地址族,必须是 AF_INET uint16_t sin_port; // 端口号(网络字节序,需 htons) struct in_addr sin_addr; // IPv4 地址(32位,网络字节序) char sin_zero[8]; // 填充,使大小与 sockaddr 一致 }; In_addr结构体 struct in_addr { in_addr_t s_addr; // 32 位 IP 地址(网络字节序) }; 它就一个成员:s_addr,类型是 in_addr_t,本质上是个 uint32_t(无符号 32 位整数) s_addr 存的是 网络字节序 的 IP 地址 |
常见用法:
struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(12345); sin.sin_addr.s_addr = inet_addr("127.0.0.1"); // 点分十进制转网络字节序 (struct sockaddr *)&sin |
网络编程中的地址转换函数主要用于 字符串地址(如 "127.0.0.1") 和二进制地址(如 in_addr 结构体)之间的相互转换,它们常用于设置 socket 地址和打印对端地址。
常用的地址转换函数一览
函数名 | 用途 | 头文件 |
inet_pton | 点分十进制 → 二进制地址 | <arpa/inet.h> |
inet_ntop | 二进制地址 → 点分十进制 | <arpa/inet.h> |
inet_addr | 字符串 → in_addr_t(过时) | <arpa/inet.h> |
inet_ntoa | in_addr → 字符串(过时) | <arpa/inet.h> |
inet_pton:点分十进制 → 二进制地址
int inet_pton(int af, const char *src, void *dst); 参数: af:地址族(AF_INET 或 AF_INET6) src:点分十进制的 IP 地址字符串,如 "192.168.1.1" dst:输出地址的指针(如 &sin_addr) 返回值: 1 转换成功,src 是合法的 IP 地址字符串 0 转换失败,src 是 无效的地址格式(例如拼错、写错) -1 转换失败,af 参数(地址族) 无效,且 errno 会被设置 示例: struct sockaddr_in addr; inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); |
inet_ntop:二进制地址 → 点分十进制
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 参数: af:地址族(AF_INET 或 AF_INET6) src:二进制格式的地址 dst:目标缓冲区,存储转换后的字符串地址 size:dst 缓冲区大小 返回值 非 NULL 指针 转换成功,返回的就是 dst 缓冲区的地址(同一个指针) NULL 转换失败,通常是 af 不合法 或 dst 太小,且 errno 被设置 示例: char ip_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &addr.sin_addr, ip_str, INET_ADDRSTRLEN); printf("IP地址是:%s\n", ip_str); //INET_ADDRSTRLEN :IPv4 字符串地址的最大长度(包含末尾的 \0 终止符)是 16 字节 |
13.2.3 TCP通信的完整流程
Socket 初始化阶段(TCP连接尚未建立)三次握手
客户端: fd = socket() 创建 socket 文件描述符,准备进行 TCP 连接。 connect(fd, 服务器地址端口) 向服务器发起连接请求(发送 SYN 报文),状态变为 SYN_SENT。 等待服务器响应,接收到 SYN+ACK 后,发送 ACK,状态变为 ESTABLISHED。 服务器: listenfd = socket() 创建监听 socket。 bind(listenfd, 地址端口) 绑定 socket 到指定 IP 和端口。 listen(listenfd, 队列长度) 把 socket 变成监听状态,状态变为 LISTEN。 connfd = accept(listenfd) 阻塞等待客户端连接请求,收到后状态变为 SYN_RCVD。 接收客户端 ACK,状态变为 ESTABLISHED,分配一个新的 connfd 与客户端通信。 三次握手: 客户端 → 服务端:SYN 客户端发送一个SYN(同步)报文,表示希望建立连接。 此时客户端进入 SYN_SEND 状态。 服务端 → 客户端:SYN + ACK 服务端收到SYN后,回复一个带有 SYN 和 ACK 标志位的报文。 表示收到请求,并同意建立连接。 服务端进入 SYN_RECEIVED 状态。 客户端 → 服务端:ACK 客户端收到 SYN+ACK 后,回复一个 ACK 报文。 表示连接建立完成。 客户端进入 ESTABLISHED 状态,服务端也进入 ESTABLISHED 状态。 |
数据通信阶段
双方都处于 ESTABLISHED 状态后,可以开始双向通信: 客户端: write(fd, buf, size):发送数据(会触发 TCP 的发送机制)。 read(fd, buf, size):阻塞等待接收数据。 通常是循环多次进行。 服务器: read(connfd, buf, size):读取客户端请求(阻塞)。 write(connfd, buf, size):写入响应数据。 同样是循环进行。 |
连接终止阶段 四次挥手
客户端主动关闭连接: close(fd): 发出 FIN 报文,状态变为 FIN_WAIT_1。 收到服务器的 ACK,进入 FIN_WAIT_2 状态。 等待服务器的 FIN 报文,收到后发送 ACK,进入 TIME_WAIT。 等待一段时间后进入 CLOSED。 服务器端: 收到 FIN 后返回 ACK,状态变为 CLOSE_WAIT。 等待处理完剩下的数据后,调用 close(connfd),发送 FIN,状态变为 LAST_ACK。 等待客户端 ACK,收到后进入 CLOSED 状态。 |
13.3 示例
13.3.1 UDP示例
服务器
#include <sys/types.h> #include <sys/socket.h> #include <iostream> #include <string> #include <stdio.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 创建一个 UDP 套接字 // AF_INET 表示 IPv4,SOCK_DGRAM 表示 UDP,0 表示默认协议 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("创建套接字失败!"); return 1; } // 定义本地地址结构体 sin 和客户端地址结构体 clr sockaddr_in sin, clr; sin.sin_family = AF_INET; // 使用 IPv4 sin.sin_addr.s_addr = INADDR_ANY; // 接收任意地址发来的数据 sin.sin_port = htons(12345); // 设置本地监听端口为 12345(需要用 htons 转换字节序) // 将套接字与本地地址绑定 if (bind(sockfd, (const struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("绑定失败!"); close(sockfd); return 1; } // 准备接收数据 char buf[1024]; // 接收缓冲区 socklen_t addl = sizeof(clr); // 用于接收客户端地址长度 // 接收客户端发来的数据 // 这里第三个参数 -1 是错误的,应为 sizeof(buf) - 1,避免越界 ssize_t n = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (sockaddr *)&clr, &addl); if (n == -1) { perror("接收数据失败!"); close(sockfd); return 1; } buf[n] = '\0'; // 添加字符串结束符,确保安全打印 std::cout << "收到消息:" << buf << std::endl; std::cout << "来自:" << inet_ntoa(clr.sin_addr) << ":" << ntohs(clr.sin_port) << std::endl; // 关闭 socket close(sockfd); return 0; } |
客户端
#include <sys/types.h> #include <sys/socket.h> #include <iostream> #include <string> #include <stdio.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 创建一个 UDP 套接字(socket) // 参数 AF_INET 表示使用 IPv4 协议 // 参数 SOCK_DGRAM 表示使用 UDP 协议 // 参数 0 表示让系统自动选择协议(对于 UDP 来说就是 IPPROTO_UDP) int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("创建套接字失败!"); return 1; } // 创建服务器的地址信息结构体 sockaddr_in ser; ser.sin_family = AF_INET; // 协议族 IPv4 ser.sin_port = htons(12345); // 服务器端口号,使用 htons 转为网络字节序 ser.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器 IP 地址(这里是本机回环地址) // 要发送的消息内容 const char *msg = "Hello from UDP client!"; // 发送数据给服务器(UDP 不需要建立连接) // 参数依次为:套接字、发送缓冲区、长度、标志、目标地址结构体、地址长度 sendto(sockfd, msg, strlen(msg), 0, (sockaddr *)&ser, sizeof(ser)); // 接收服务器返回的数据 char buffer[1024]; // 接收缓冲区 socklen_t len = sizeof(ser); // 地址结构体长度,用于 recvfrom 的最后一个参数 int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&ser, &len); // 接收数据,存入 buffer if (n < 0) { perror("接受失败"); } else { buffer[n] = '\0'; // 手动添加字符串结束符 std::cout << "从服务器接受: " << buffer << std::endl; } // 关闭套接字,释放资源 close(sockfd); return 0; } |
Makefile
all:client server client:client.cpp g++ -o client client.cpp server:server.cpp g++ -o server server.cpp .PHONY:clean clean: rm -f client server |
13.3.2 TCP示例
服务器
#include <sys/types.h> #include <sys/socket.h> #include <iostream> #include <stdio.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 创建 TCP 套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("创建套接字失败!"); return 1; } // 配置服务器地址 sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(12345); sin.sin_addr.s_addr = INADDR_ANY; // 绑定 socket 到本地地址 if (bind(sockfd, (const sockaddr *)&sin, sizeof(sin)) < 0) { perror("绑定失败!"); close(sockfd); return 1; } // 开始监听客户端连接 if (listen(sockfd, 1) < 0) { perror("监听失败!"); close(sockfd); return 1; } std::cout << "等待客户端连接......" << std::endl; sockaddr_in cli; socklen_t cli_t = sizeof(cli); int sockfd_cli = accept(sockfd, (struct sockaddr *)&cli, &cli_t); if (sockfd_cli < 0) { perror("接受连接失败!"); close(sockfd); return 1; } // 接收客户端数据 char buf[1024]; int read_ser = read(sockfd_cli, buf, sizeof(buf) - 1); // 从连接套接字读取数据 if (read_ser < 0) { perror("读取数据失败!"); close(sockfd_cli); close(sockfd); return 1; } buf[read_ser] = '\0'; // 添加字符串结束符 std::cout << "读取的数据:" << buf << std::endl; // 回复客户端 char buf_write[] = "你好"; int write_ser = write(sockfd_cli, buf_write, sizeof(buf_write)); if (write_ser < 0) { perror("发送数据失败!"); close(sockfd_cli); close(sockfd); return 1; } // 关闭连接 close(sockfd_cli); close(sockfd); return 0; } |
客户端
#include <sys/types.h> #include <sys/socket.h> #include <iostream> #include <stdio.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main() { // 创建 TCP 套接字 int clifd = socket(AF_INET, SOCK_STREAM, 0); if (clifd < 0) { perror("创建套接字失败!"); return 1; } // 设置服务器地址信息 sockaddr_in clin; clin.sin_family = AF_INET; clin.sin_port = htons(12345); inet_pton(AF_INET, "127.0.0.1", &clin.sin_addr); // 设置本地环回地址 // 连接服务器 if (connect(clifd, (const sockaddr *)&clin, sizeof(clin)) < 0) { perror("建立连接失败!"); close(clifd); return 1; } // 要发送的数据 char buf[] = "不知道说什么"; // 写数据 int write_cli = write(clifd, buf, sizeof(buf)); if (write_cli < 0) { perror("发送数据失败!"); close(clifd); return 1; } // 接收服务器回复 char buffer[1024] = {0}; int n = read(clifd, buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = '\0'; // 添加字符串结束符 std::cout << "收到服务器回复:" << buffer << std::endl; } else if (n < 0) { perror("读取失败!"); } close(clifd); return 0; } |
Makefile
all:client server client:client.cpp g++ -o client client.cpp server:server.cpp g++ -o server server.cpp .PHONY:clean clean: rm -f client server |
13.3.3多进程示例
服务器
// server_fork.cpp #include <sys/types.h> #include <sys/socket.h> #include <iostream> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <signal.h> int main() { int listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); return 1; } sockaddr_in server_addr{}; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(12345); server_addr.sin_addr.s_addr = INADDR_ANY; if (bind(listen_fd, (sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); return 1; } if (listen(listen_fd, 5) < 0) { perror("listen"); return 1; } std::cout << "服务器启动,等待客户端连接..." << std::endl; signal(SIGCHLD, SIG_IGN); // 避免僵尸进程 while (true) { sockaddr_in client_addr{}; socklen_t client_len = sizeof(client_addr); int conn_fd = accept(listen_fd, (sockaddr *)&client_addr, &client_len); if (conn_fd < 0) { perror("accept"); continue; } pid_t pid = fork(); if (pid == 0) { // 子进程 close(listen_fd); // 子进程不用监听 socket char buffer[1024] = {0}; int n = read(conn_fd, buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = '\0'; std::cout << "收到客户端[" << inet_ntoa(client_addr.sin_addr) << "]: " << buffer << std::endl; const char *reply = "你好客户端,我是服务器!"; write(conn_fd, reply, strlen(reply)); } close(conn_fd); return 0; } else if (pid > 0) { // 父进程 close(conn_fd); // 父进程不处理这个连接 } else { perror("fork"); } } close(listen_fd); return 0; } |
客户端
// client.cpp #include <sys/types.h> #include <sys/socket.h> #include <iostream> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <stdio.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket"); return 1; } sockaddr_in server_addr{}; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(12345); inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); if (connect(sockfd, (sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("connect"); return 1; } const char *msg = "你好服务器!"; write(sockfd, msg, strlen(msg)); char buffer[1024] = {0}; int n = read(sockfd, buffer, sizeof(buffer) - 1); if (n > 0) { buffer[n] = '\0'; std::cout << "收到服务器回复:" << buffer << std::endl; } close(sockfd); return 0; } |