字节序
字节序是计算机中多字节数据存储或传输时各字节的排列顺序。其核心区别在于高位字节与低位字节在内存中的存放顺序,主要分为大端序和小端序两种类型
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址
socket编程步骤
工作流程简述
创建一个 socket 仅仅是拿到了一个“门户”,要真正开始通信,还需要根据你是客户端还是服务端进行后续操作:
客户端典型流程
socket()
– 创建 socket。connect()
– 主动连接到服务器。send() / recv()
– 与服务器进行数据交换。close()
– 关闭连接。
服务端典型流程
socket()
– 创建 socket。bind()
– 将 socket 绑定到一个本地 IP 地址和端口号。listen()
– 监听来自客户端的连接请求(仅用于 TCP)。accept()
– 接受一个连接,得到一个新的用于通信的 socket。send() / recv()
– 与连接的客户端进行数据交换。close()
– 关闭连接。
socket函数 创建 socket
socket
函数是网络编程中最核心、最基础的函数,它是创建网络通信端点的起点。无论是像浏览器这样的客户端,还是像Web服务器这样的服务端,一切网络通信都始于调用 socket
函数。
核心概念
你可以把 socket
想象成 “网络通信的端点” 或者 “网络数据交换的门户”。应用程序要通过网络发送或接收数据,必须首先创建一个 socket。这个 socket 在操作系统中被标识为一个组合,通常包括:
- IP 地址:标识网络中的一台主机。
- 端口号:标识主机上的一个特定应用程序。
- 协议类型:定义如何传输数据(如 TCP 或 UDP)。
socket
函数的作用就是向操作系统申请并创建这样一个通信端点,并返回一个标识符(文件描述符)以供后续操作使用。
函数原型(C语言)
通常在头文件 <sys/socket.h>
中定义。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数解析
这个函数有三个参数,它们共同决定了创建的 socket 所具有的特性:
domain
(协议族/地址族)- 指定 socket 将使用哪种地址体系进行通信。
- 常见值:
AF_INET
: IPv4 网络协议,这是最常用的家族,使用 32 位 IP 地址(如192.168.1.1
)。AF_INET6
: IPv6 网络协议,使用 128 位 IP 地址。AF_UNIX
或AF_LOCAL
: 本地通信,用于同一台机器上的进程间通信,使用文件路径名作为地址。
type
( socket 类型)- 指定了 socket 的通信语义,即数据传输的方式。
- 常见值:
SOCK_STREAM
: 提供面向连接的、可靠的、双向字节流通信。它使用 TCP 协议。 characteristics: 数据无丢失、无重复、按序到达。就像打电话,需要先建立连接,通信可靠。SOCK_DGRAM
: 提供无连接的、不可靠的数据报通信。它使用 UDP 协议。 characteristics: 尽力传输,可能丢失或乱序,但开销小,速度快。就像发短信,无需连接,直接发送。SOCK_RAW
: 原始套接字,允许对底层协议(如 IP 或 ICMP)进行直接访问,可以手动构造协议头。常用于网络探测或实现自定义协议。
protocol
(协议)- 通常设置为
0
,表示由系统根据domain
和type
的组合自动选择默认的协议。 - 例如:
(AF_INET, SOCK_STREAM, 0)
会自动选择 TCP 协议。(AF_INET, SOCK_DGRAM, 0)
会自动选择 UDP 协议。
- 只有在需要某种特定协议时(如在原始套接字中指定 ICMP),才需要显式指定协议号(如
IPPROTO_ICMP
)。
- 通常设置为
返回值
- 成功: 返回一个新的** socket 文件描述符**(一个非负整数)。后续的所有操作(如连接、发送、接收、关闭)都将使用这个描述符来引用这个 socket。
- 失败: 返回
-1
,并设置全局变量errno
以指示错误类型(如EACCES
权限不足、EMFILE
进程打开文件数达到上限等)。
创建一个使用 TCP 协议的 IPv4 socket
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int sockfd;
// 创建 socket
// AF_INET: IPv4
// SOCK_STREAM: TCP
// 0: 自动选择协议
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
printf("Socket created successfully. File descriptor: %d\n", sockfd);
// ... 这里可以进行 bind, connect, listen 等后续操作 ...
// 最后不要忘记关闭 socket
close(sockfd);
return 0;
}
创建一个使用 UDP 协议的 IPv4 socket
// 只需将 type 参数改为 SOCK_DGRAM
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
inet_aton函数 将点分十进制格式的IPv4地址字符串
inet_aton
(Address TO Number)是一个用于将点分十进制格式的IPv4地址字符串(如 “192.168.1.1”)转换为网络字节序的32位二进制整数的函数。这个二进制形式可以直接用于 struct sockaddr_in
的 sin_addr.s_addr
字段。
函数原型
在头文件 <arpa/inet.h>
中声明。
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
参数解析
const char *cp
- 这是一个输入参数,指向一个以空字符结尾的字符串,该字符串包含要转换的点分十进制IPv4地址。
- 例如:
"192.168.142.128"
、"127.0.0.1"
或"8.8.8.8"
。
struct in_addr *inp
- 这是一个输出参数,是一个指向
struct in_addr
的指针。函数会将转换后的32位网络字节序的IPv4地址存储到这个结构体的s_addr
字段中。 struct in_addr
通常定义如下:
- 这是一个输出参数,是一个指向
struct in_addr {
in_addr_t s_addr; // 32位的IPv4地址(网络字节序)
};
返回值
- 成功: 返回 1(非零值)。
- 失败(如果输入的字符串不是有效的IPv4地址): 返回 0。
注意: 与许多C库函数不同,inet_aton
成功时返回1,失败返回0,而不是返回-1并设置errno。
功能详解
inet_aton
完成了一个关键任务:地址表示形式的转换。
- 人类可读形式 -> 机器可读形式
- 输入:
"192.168.142.128"
(字符串) - 输出:
0x808ea8c0
(32位整数,网络字节序)
网络字节序: 转换后的整数是大端模式(Big-endian),这是TCP/IP协议栈要求的标准字节序。这意味着最重要的字节(最高位字节)存储在最低的内存地址上。
- 输入:
inet_ntoa函数 将网络格式的ip地址转为字符串格式
函数概述
inet_ntoa
(Network TO Address)是一个用于将网络字节序的32位二进制IPv4地址转换回点分十进制格式字符串的函数。它执行的是 inet_aton
的逆操作。
函数原型
在头文件 <arpa/inet.h>
中声明。
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
参数解析
struct in_addr in
- 这是一个输入参数,是一个
struct in_addr
结构体(而不是指针),其中包含要转换的32位网络字节序IPv4地址。 - 结构体定义通常为:
- 这是一个输入参数,是一个
struct in_addr {
in_addr_t s_addr; // 32位的IPv4地址(网络字节序)
};
返回值
- 返回一个指向静态分配缓冲区的指针,该缓冲区中包含点分十进制格式的IPv4地址字符串。
- 这个函数不需要手动释放内存,但也不应该长期保存返回的指针。
功能详解
inet_ntoa
完成了一个关键任务:二进制地址到人类可读形式的转换。
- 机器可读形式 -> 人类可读形式
- 输入:
0x808ea8c0
(32位整数,网络字节序) - 输出:
"192.168.142.128"
(字符串)
- 输入:
重要特性: 函数会自动处理网络字节序到主机字节序的转换,你不需要先调用 ntohl
。
bind函数 和socket关联起来
bind
函数是网络编程中服务器端程序至关重要的一步。它的核心作用是将一个 socket 与一个特定的本地网络地址(IP 地址 + 端口号)相关联。
你可以这样理解:
socket()
函数就像是买了一部手机。bind()
函数就像是给这部手机插上一张 SIM 卡并分配一个手机号码。- 如果没有手机号(
bind
),别人就无法通过一个固定的号码联系到你。
- 如果没有手机号(
- 对于服务器来说,它必须有一个固定且众所周知的地址,这样客户端才知道要连接到哪里。
bind
就是完成这个“公布地址”动作的函数。
函数原型(C语言)
通常在头文件 <sys/socket.h>
中定义。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解析
int sockfd
- 这是
socket()
函数成功调用后返回的 socket 文件描述符。bind
操作就是要给这个特定的 socket 分配地址。
- 这是
const struct sockaddr *addr
- 这是一个指向通用地址结构体的指针。它指向一个包含了你要绑定到的 IP 地址和端口号的结构体。
- 这是一个通用类型。在实际使用时,我们需要传入特定协议族的地址结构(如
struct sockaddr_in
for IPv4),但在函数调用时,必须强制转换为struct sockaddr *
类型。
socklen_t addrlen
- 这是第二个参数
addr
所指向的结构体的长度(以字节为单位)。通常使用sizeof(struct sockaddr_in)
来获取。
- 这是第二个参数
返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置全局变量errno
以指示错误类型。最常见的错误是EADDRINUSE
(指定的地址和端口已被占用)。
核心工作:填充地址结构体 sockaddr_in
对于最常用的 IPv4 (AF_INET
),我们使用 struct sockaddr_in
来存储地址信息。在调用 bind
之前,必须正确地填充这个结构体。
struct sockaddr_in
结构(定义在 <netinet/in.h>
)
struct sockaddr_in {
sa_family_t sin_family; // 地址族: AF_INET (IPv4)
in_port_t sin_port; // 网络字节序的端口号
struct in_addr sin_addr; // IPv4 地址结构
char sin_zero[8]; // 填充字段,通常设置为0
};
struct in_addr {
uint32_t s_addr; // 网络字节序的IPv4地址
};
填充这个结构体的关键步骤:
- 设置地址族 (
sin_family
)- 直接设置为
AF_INET
。
- 直接设置为
- 设置端口号 (
sin_port
)- 端口号必须是网络字节序(大端模式)。我们通常使用
htons()
(Host TO Network Short) 函数将主机字节序的端口号(一个16位的短整型)进行转换。 - 例如:
my_addr.sin_port = htons(8080); // 绑定到8080端口
- 端口号必须是网络字节序(大端模式)。我们通常使用
- 设置 IP 地址 (
sin_addr.s_addr
)- IP 地址也必须是网络字节序。我们可以使用:
INADDR_ANY
:这是一个特殊的常量(通常是0.0.0.0
),表示绑定到机器上所有可用的网络接口。服务器通常这样设置,这样无论客户端通过哪个网卡(如以太网、Wi-Fi)连过来,服务器都能接收到连接。这是最常见的用法。inet_addr("192.168.1.100")
:将点分十进制的IP字符串转换为网络字节序的32位整数。- 直接指定一个特定的IP地址(需要先转换)。
- 例如:
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地IP
- IP 地址也必须是网络字节序。我们可以使用:
示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h> // 包含 sockaddr_in 结构
int main() {
int server_fd;
struct sockaddr_in server_addr;
// 1. 创建 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 准备地址结构体
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地IP
server_addr.sin_port = htons(8080); // 绑定到8080端口
// 3. 调用 bind
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server successfully bound to port 8080 on all interfaces.\n");
// ... 后续可以调用 listen, accept 等 ...
close(server_fd);
return 0;
}
listen函数 监听
listen
函数是TCP服务器端编程中一个承上启下的关键步骤。它的作用非常明确:将一个已绑定的 socket 从“主动”转换为“被动”模式,并通知操作系统开始监听来自客户端的连接请求。
核心概念与类比
一个完美的类比是 “电话总机”:
socket()
:你买了一部电话机。bind()
:你给这部电话机申请了一个固定的电话号码(如 400-123-4567)listen()
:你打开电话机的响铃功能并插上电源,告诉系统和外界:“我的这个号码已经准备就绪,可以接听来电了”。同时,你设置了一个“呼叫等待队列”的长度,决定最多能有多少个电话同时打进来等待接听。accept()
:当电话铃响时,你拿起听筒,与对方建立真正的通话连接。
如果没有调用 listen
,即使你的 socket 已经绑定了地址,它也会对 incoming 的连接请求置之不理,操作系统会直接拒绝(RST)客户端的连接尝试。
函数原型(C语言)
在头文件 <sys/socket.h>
中定义。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数解析
int sockfd
- 这是
socket()
函数创建并由bind()
函数绑定了地址的 socket 文件描述符。这个 socket 必须是SOCK_STREAM
类型(即TCP socket)。
- 这是
int backlog
- 这是整个函数中最关键且容易误解的参数。它定义了该 socket 的未完成连接队列和已完成连接队列的总和的最大长度。
- 含义: 当多个客户端同时发起连接时,TCP的三次握手需要时间来完成。
backlog
参数限制了在这些握手过程处于不同阶段时,系统能为该 socket 排队等待处理的连接请求的最大数量。 - 队列详解:
- 未完成连接队列 (SYN队列): 存放已收到客户端SYN包、正在等待完成三次握手的连接。状态为
SYN_RCVD
。 - 已完成连接队列 (Accept队列): 存放已经完成三次握手、等待服务器调用
accept()
来取走的连接。状态为ESTABLISHED
。
- 未完成连接队列 (SYN队列): 存放已收到客户端SYN包、正在等待完成三次握手的连接。状态为
- 如何设置:
- 历史上,这个参数的含义在不同系统中变化很大。现代Linux系统通常将其定义为 已完成连接队列 (Accept队列) 的最大长度。
- 这是一个建议值,内核会根据实际情况进行调整(例如,在
/proc/sys/net/core/somaxconn
中定义了系统级别的全局最大值)。 - 对于高并发服务器,通常会设置为一个较大的值(如 128, 1024 或更多),但不应超过系统的
somaxconn
限制。 - 如果队列已满时又有新的连接完成握手,服务器可能会忽略客户端发来的ACK包,导致客户端认为连接已建立,而服务器却将其丢弃。
返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置全局变量errno
以指示错误类型。- 常见的错误:
EBADF
(无效的文件描述符)、EINVAL
(socket未绑定或已连接)、ENOTSOCK
(文件描述符不是socket)。
- 常见的错误:
简单示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BACKLOG 10 // 定义等待队列的最大长度
int main() {
int server_fd;
struct sockaddr_in server_addr;
// 1. 创建 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 绑定地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地网卡
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 3. 开始监听!
if (listen(server_fd, BACKLOG) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server is now listening on port %d...\n", PORT);
// ... 后续会进入循环,调用 accept() 来接受连接 ...
// int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen);
close(server_fd);
return 0;
}
accept函数 拿起听筒,并创建一个通话通道
accept
函数的作用是:从已完成连接队列(Accept Queue)中取出第一个连接请求,为这个连接创建一个新的socket,并返回其文件描述符。
核心概念与完美类比
继续使用“电话总机”的类比:
socket()
: 买一部电话机。bind()
: 申请一个电话号码(INADDR_ANY:8080
)。listen()
: 打开电话响铃,并设置呼叫等待队列容量(backlog
)。现在电话可以响了。accept()
: 电话铃响了!你拿起听筒(accept
)。这个动作:- 返回一个新的通话通道:你手里拿着的听筒(返回的新socket)是专门用于和这位来电者对话的。
- 总机保持空闲:你原来的那部主机(监听socket)被释放出来,立刻放回桌上,继续等待下一个来电。它永远不会用于直接通话,只负责接听新的呼叫。
这个类比精确地解释了为什么需要两个socket:
- 监听Socket (Listening Socket): 由
socket()
创建,bind()
和listen()
设置。它就像一个总机接线员,只负责接受新的呼叫请求,不进行实际的数据通信。 - 已连接Socket (Connected Socket): 由
accept()
返回。它代表一个已经建立好的、唯一的网络连接,用于与特定的客户端进行send()
和recv()
操作。
函数原型(C语言)
在头文件 <sys/socket.h>
中定义。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数解析
int sockfd
- 这是
listen()
函数正在监听的监听socket的文件描述符。这个socket必须已经成功调用了bind()
和listen()
。
- 这是
struct sockaddr *addr
- 这是一个输出参数。它是一个指向缓冲区的指针,
accept
函数会在其中填充发起连接请求的那个客户端的地址信息(IP地址和端口号)。 - 如果你不关心客户端的地址,可以简单地设置为
NULL
。
- 这是一个输出参数。它是一个指向缓冲区的指针,
socklen_t *addrlen
- 这是一个输入输出参数。
- 输入时:它是一个指针,指向一个整数,该整数表示你提供的
addr
缓冲区的长度。 - 输出时:
accept
函数会修改它指向的值,将其设置为实际写入addr
中的地址信息的真实长度。 - 如果
addr
为NULL
,那么这个参数也应该是NULL
。
返回值
- 成功: 返回一个新的、非负的文件描述符。这个新的socket描述符就是已连接socket,用于与刚刚接受的客户端进行通信。
- 失败: 返回
-1
,并设置全局变量errno
以指示错误类型(例如EBADF
,EINVAL
,ECONNABORTED
)。
工作流程与行为
- 阻塞行为: 默认情况下,如果已完成连接队列为空(即没有客户端连接上来),
accept()
函数会阻塞(使程序暂停执行),直到有新的连接到达。 - 取出连接: 一旦有连接可用,
accept
就从队列中取出第一个连接。 - 创建新Socket: 系统为这个连接创建一个全新的socket。这个新socket与监听socket的地址族(AF_INET)和类型(SOCK_STREAM)相同,但它已经与一个特定的远程客户端peer关联好了。
- 返回并通信: 函数返回新socket的文件描述符。服务器随后使用这个新描述符与客户端进行读写操作。
关键点: 调用 accept()
并不会影响原始的监听socket sockfd
。该监听socket仍然保持打开状态,可以继续用于下一次 accept()
调用来接受新的连接。这就是服务器能够同时处理多个客户端的基础。
示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // 用于 inet_ntoa
#define PORT 8080
#define BACKLOG 5
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char client_ip[INET_ADDRSTRLEN];
// 1. 创建、绑定、监听 socket (步骤1-3)
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... 错误检查 ...
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(server_fd, BACKLOG) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 2. 主循环:等待并接受连接
while(1) {
// accept() 会阻塞,直到有客户端连接
printf("Waiting for a connection...\n");
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd < 0) {
perror("accept failed");
continue; // 不要退出,继续接受下一个连接
}
// 将客户端的IP地址从网络字节序转换为可读字符串
inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
printf("Connection accepted from %s:%d\n", client_ip, ntohs(client_addr.sin_port));
// 3. 现在可以使用 client_fd 与客户端通信!
// 例如:send(client_fd, "Hello, Client!", 14, 0);
// 通常这里会fork()一个子进程或创建一个新线程来处理这个客户端,
// 这样主线程就可以立刻回到 accept() 等待下一个客户端。
// 4. 通信完毕,关闭这个客户端的连接socket。
// (注意:不能关闭 server_fd!)
close(client_fd);
printf("Connection with %s closed.\n", client_ip);
}
// 通常不会执行到这里,但如果需要退出,应关闭 server_fd
close(server_fd);
return 0;
}
connect函数 主动与服务器建立连接
connect
函数是 TCP客户端 编程中的核心函数,它的作用是主动与服务器建立连接。对于TCP协议,它会触发TCP三次握手过程;对于UDP协议,它则只是设置默认的目标地址。
函数原型(C语言)
在头文件 <sys/socket.h>
中定义。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解析
int sockfd
- 这是通过
socket()
函数创建的客户端socket的文件描述符。 - 这个socket必须是未连接的,并且通常是
SOCK_STREAM
(TCP) 或SOCK_DGRAM
(UDP) 类型。
- 这是通过
const struct sockaddr *addr
- 这是一个指向服务器地址结构体的指针,包含了客户端想要连接的服务器的IP地址和端口号
- 与
bind()
和accept()
类似,这里需要传入特定协议族的地址结构(如struct sockaddr_in
for IPv4),但在函数调用时必须强制转换为struct sockaddr *
类型。
socklen_t addrlen
- 这是第二个参数
addr
所指向的结构体的长度(以字节为单位)。 - 通常使用
sizeof(struct sockaddr_in)
。
- 这是第二个参数
返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置全局变量errno
以指示错误类型。
功能详解
connect
函数的行为根据socket类型的不同而有所不同:
1. 对于TCP socket (SOCK_STREAM
)
- 触发三次握手:
connect
会主动发起TCP三次握手过程,与指定的服务器建立可靠的连接。 - 阻塞行为: 默认情况下,
connect
会阻塞,直到连接成功建立或发生错误。 - 连接成功: 只有三次握手完成后,
connect
才会返回成功。此后,客户端可以使用send()
和recv()
与服务器进行通信。
2. 对于UDP socket (SOCK_DGRAM
)
- 不建立实际连接: UDP是无连接的,所以
connect
不会触发任何网络握手过程。 - 设置默认目标: 它只是在内核中记录服务器的地址信息,设置为后续
send()
和recv()
调用的默认目标。 - 作用:
- 之后可以直接使用
send()
而不是sendto()
(因为目标地址已已知) - 可以使用
recv()
而不是recvfrom()
(但只会接收来自该地址的数据报) - 提高效率:避免每次发送数据时重复指定目标地址
- 之后可以直接使用
gets 函数 标准输入
函数概述
gets
函数曾经是C标准库(<stdio.h>
)中的一个函数,用于从标准输入(stdin) 读取一行字符串,直到遇到换行符或文件结束符。
函数原型
char *gets(char *str);
参数解析
char *str
- 这是一个指向字符数组(缓冲区)的指针,用于存储读取的字符串。
- 缓冲区应该足够大,以容纳用户可能输入的任何内容。
返回值
- 成功: 返回传入的
str
指针。 - 失败或到达文件末尾: 返回
NULL
。
sprintf函数 格式化数据写入字符串
sprintf
是 C 语言中一个非常重要且强大的函数,用于将格式化数据写入字符串。它的名称代表 “String PRINT Formatted”。
函数原型
在头文件 <stdio.h>
中声明。
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
参数解析
char *str
- 这是一个指向目标缓冲区的指针,格式化后的字符串将存储在这里。
- 非常重要:你必须确保这个缓冲区足够大,能够容纳格式化后的所有内容,包括结尾的空字符
\0
。
const char *format
- 这是一个格式化字符串,包含要写入的文本和格式说明符(以
%
开头的特殊标记)。 - 格式说明符指定了后续参数如何被格式化并插入到字符串中。
- 这是一个格式化字符串,包含要写入的文本和格式说明符(以
...
(可变参数)- 这是零个或多个附加参数,这些参数的值将根据格式字符串中的格式说明符被格式化和插入。
返回值
- 成功:返回写入目标字符串的字符数量(不包括结尾的空字符)。
- 失败:返回一个负数。
格式说明符
sprintf
支持丰富的格式说明符,以下是一些常用的:
格式说明符 | 描述 | 示例 |
---|---|---|
%d 或 %i | 有符号十进制整数 | sprintf(buf, "%d", 42) → "42" |
%u | 无符号十进制整数 | sprintf(buf, "%u", 42) → "42" |
%f | 浮点数 | sprintf(buf, "%f", 3.14) → "3.140000" |
%c | 字符 | sprintf(buf, "%c", 'A') → "A" |
%s | 字符串 | sprintf(buf, "%s", "hello") → "hello" |
%x | 十六进制整数(小写) | sprintf(buf, "%x", 255) → "ff" |
%X | 十六进制整数(大写) | sprintf(buf, "%X", 255) → "FF" |
%o | 八进制整数 | sprintf(buf, "%o", 8) → "10" |
%p | 指针地址 | sprintf(buf, "%p", &var) → "0x7ffd42a" |
%% | 百分号字符本身 | sprintf(buf, "%%") → "%" |
格式说明符可以包含修饰符来控制输出: |
%10s
– 最小宽度为10字符,右对齐%-10s
– 最小宽度为10字符,左对齐%.2f
– 浮点数保留2位小数%5.2f
– 总宽度5字符,保留2位小数
服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // 用于 inet_ntoa
int main()
{
int s_fd;//socket返回的标识符
s_fd = socket(AF_INET, SOCK_STREAM, 0);//创建 socket,以IPV4,TCP协议创建
if(s_fd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in s_addr;//关联socket的结构体变量
s_addr.sin_family = AF_INET;//设为IPV4
s_addr.sin_port = htons(8989);//字节序的端口号,htons绑定端口
inet_aton("127.0.0.1", &s_addr.sin_addr);//将点分十进制的IP字符串转换为网络字节序的32位整数。
bind(s_fd, (struct sockaddr *)&s_addr, sizeof(s_addr));//socket标识符,关联socket的结构体变量地址,结构体大小
listen(s_fd, 10);//监听(socket标识符,连接队列长度)
int c_fd = accept(s_fd, NULL, NULL);//连接请求接通(socket标识符,,)
printf("connect\n");//接通了就打印
while(1);
return 0;
}
有读写功能
#include <stdio.h> // 标准输入输出函数,如printf, perror
#include <stdlib.h> // 标准库函数,如exit
#include <string.h> // 字符串处理函数,如memset
#include <unistd.h> // Unix标准函数,如read, write, close
#include <sys/socket.h> // 套接字相关函数和数据结构
#include <netinet/in.h> // Internet地址族相关定义,如sockaddr_in
#include <arpa/inet.h> // IP地址转换函数,如inet_aton, inet_ntoa
int main()
{
int s_fd; // 服务器监听socket的文件描述符
int n_read; // 读取到的字节数
char readBuf[128]; // 读取数据的缓冲区
char *msg = "I get your connect"; // 要发送给客户端的消息
struct sockaddr_in s_addr; // 服务器地址结构体
struct sockaddr_in c_addr; // 客户端地址结构体
// 清空地址结构体,确保没有垃圾数据
memset(&s_addr, 0, sizeof(struct sockaddr_in));
memset(&c_addr, 0, sizeof(struct sockaddr_in));
// 创建socket,AF_INET表示IPv4,SOCK_STREAM表示TCP协议,0表示使用默认协议
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1) // 检查socket创建是否成功
{
perror("socket"); // 打印错误信息
exit(-1); // 退出程序
}
// 设置服务器地址参数
s_addr.sin_family = AF_INET; // 地址族设为IPv4
s_addr.sin_port = htons(8989); // 端口号转换为网络字节序,绑定到8989端口
// 将点分十进制的IP字符串转换为网络字节序的32位整数,并存入地址结构体
inet_aton("192.168.142.128", &s_addr.sin_addr);
// 将socket与服务器地址绑定
bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
// 开始监听连接请求,设置连接队列最大长度为10
listen(s_fd, 10);
// 客户端地址结构体长度
int clen = sizeof(struct sockaddr_in);
// 接受客户端连接请求,返回用于通信的新socket描述符
// 同时会填充客户端地址信息到c_addr中
int c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
if(c_fd == -1) // 检查accept是否成功
{
perror("accept");
exit(-1);
}
// 打印连接的客户端IP地址(从网络字节序转换为点分十进制字符串)
printf("connect %s\n", inet_ntoa(c_addr.sin_addr));
// 从客户端socket读取数据到缓冲区,最多读取128字节
n_read = read(c_fd, readBuf, sizeof(readBuf));
if(n_read == -1) // 检查read是否成功
{
perror("read");
exit(-1);
}
else
{
// 打印读取到的字节数和内容
printf("get message:%d.%s\n", n_read, readBuf);
}
// 向客户端socket写入消息
write(c_fd, msg, strlen(msg));
close(c_fd);//关闭socket
close(s_fd);
return 0;
}
socket客户端代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // 用于 inet_ntoa
int main()
{
int c_fd;//socket返回的标识符
int n_read;
char readBuf[128];
char *msg = "msg from client";
struct sockaddr_in c_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
c_fd = socket(AF_INET, SOCK_STREAM, 0);//创建 socket,以IPV4,TCP协议创建
if(c_fd == -1)
{
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;//设为IPV4
c_addr.sin_port = htons(8989);//字节序的端口号,htons绑定端口
inet_aton("192.168.142.128", &c_addr.sin_addr);//将点分十进制的IP字符串转换为网络字节序的32位整数。
if(connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr_in)) == -1)
{
perror("connect");
exit(-1);
}
if(write(c_fd, msg, strlen(msg)) == -1)
{
perror("write");
exit(-1);
}
n_read = read(c_fd, readBuf, sizeof(readBuf));
if(n_read == -1)
{
perror("read");
exit(-1);
}
else
{
printf("get message from server:%d.%s\n",n_read, readBuf);
}
return 0;
}
接收多方客户端信息
server.c
#include <stdio.h> // 标准输入输出头文件
#include <sys/types.h> // 提供数据类型定义
#include <sys/socket.h> // 提供socket函数及相关数据结构
//#include <linux/in.h> // Linux特定的Internet地址定义(已注释掉)
#include <netinet/in.h> // 提供sockaddr_in等Internet地址结构
#include <arpa/inet.h> // 提供IP地址转换函数
#include <stdlib.h> // 提供exit、atoi等标准库函数
#include <string.h> // 提供字符串处理函数
//chenlichen (可能是作者姓名或注释标记)
int main(int argc, char **argv)
{
int s_fd; // 服务器套接字文件描述符
int c_fd; // 客户端连接套接字文件描述符
int n_read; // 读取数据的字节数
char readBuf[128]; // 读取数据的缓冲区
int mark = 0; // 客户端计数器
char msg[128] = {0}; // 发送消息的缓冲区
// char *msg = "I get your connect"; // 硬编码消息(已注释)
struct sockaddr_in s_addr; // 服务器地址信息结构体
struct sockaddr_in c_addr; // 客户端地址信息结构体
// 检查命令行参数数量是否正确(程序名 + IP地址 + 端口号)
if(argc != 3){
printf("param is not good\n"); // 参数不正确时输出提示
exit(-1); // 退出程序
}
// 初始化服务器和客户端地址结构体为0
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1. 创建套接字
s_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建IPv4 TCP套接字
if(s_fd == -1){ // 检查套接字创建是否成功
perror("socket"); // 输出错误信息
exit(-1); // 退出程序
}
// 配置服务器地址信息
s_addr.sin_family = AF_INET; // 使用IPv4地址族
s_addr.sin_port = htons(atoi(argv[2])); // 设置端口号(转换为网络字节序)
inet_aton(argv[1],&s_addr.sin_addr); // 将字符串IP地址转换为二进制格式
//2. 绑定套接字到指定地址和端口
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3. 监听连接请求,设置最大等待连接数为10
listen(s_fd,10);
//4. 接受客户端连接
int clen = sizeof(struct sockaddr_in); // 客户端地址结构体长度
while(1){ // 主循环,持续接受客户端连接
// 接受客户端连接
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){ // 检查接受连接是否成功
perror("accept"); // 输出错误信息
}
mark++; // 增加客户端计数器
// 打印客户端IP地址
printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));
// 创建子进程处理客户端请求
if(fork() == 0){ // 子进程
// 创建孙子进程定期发送欢迎消息
if(fork()==0){ // 孙子进程
while(1){ // 循环发送欢迎消息
// 格式化欢迎消息
sprintf(msg,"welcom No.%d client",mark);
// 发送消息到客户端
write(c_fd,msg,strlen(msg));
sleep(3); // 等待3秒
}
}
//5. 读取客户端数据
while(1){
memset(readBuf,0,sizeof(readBuf)); // 清空读取缓冲区
n_read = read(c_fd, readBuf, 128); // 从客户端读取数据
if(n_read == -1){ // 检查读取是否出错
perror("read"); // 输出读取错误信息
}else if(n_read>0){ // 成功读取到数据
printf("\nget: %d\n",n_read); // 输出读取的字节数
}else{ // 读取到0字节(客户端断开连接)
printf("client quit\n"); // 输出客户端退出信息
break; // 退出循环
}
}
break; // 退出子进程
}
}
return 0; // 程序结束
}
client.c
#include <stdio.h> // 标准输入输出函数库
#include <sys/types.h> // 定义数据类型,如size_t、ssize_t等
#include <sys/socket.h> // 套接字编程接口
//#include <linux/in.h> // Linux IPv4头文件(已被注释掉)
#include <netinet/in.h> // Internet地址族结构体定义
#include <arpa/inet.h> // IP地址转换函数
#include <stdlib.h> // 标准库函数,如exit、atoi等
#include <string.h> // 字符串处理函数
int main(int argc, char **argv)
{
int c_fd; // 客户端套接字文件描述符
int n_read; // 读取数据的字节数
char readBuf[128]; // 用于存储从服务器读取的数据的缓冲区
int tmp; // 未使用的临时变量
// char *msg = "msg from client"; // 硬编码消息(已被注释掉)
char msg[128] = {0}; // 用于存储用户输入的消息的缓冲区
struct sockaddr_in c_addr; // 服务器地址信息结构体
memset(&c_addr,0,sizeof(struct sockaddr_in)); // 将地址结构体清零
// 检查命令行参数数量是否正确
if(argc != 3){
printf("param is not good\n"); // 参数不正确时输出提示
exit(-1); // 退出程序
}
printf("%d\n",getpid()); // 打印当前进程ID
// 1. 创建套接字
c_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建IPv4 TCP套接字
if(c_fd == -1){ // 检查套接字创建是否成功
perror("socket"); // 输出错误信息
exit(-1); // 退出程序
}
// 设置服务器地址信息
c_addr.sin_family = AF_INET; // 使用IPv4地址族
c_addr.sin_port = htons(atoi(argv[2])); // 设置端口号(转换为网络字节序)
inet_aton(argv[1],&c_addr.sin_addr); // 将字符串IP地址转换为二进制格式
// 2.连接到服务器
if(connect(c_fd, (struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){
perror("connect"); // 输出连接错误信息
exit(-1); // 退出程序
}
while(1){ // 主循环
// 创建子进程处理用户输入
if(fork()==0){ // 子进程代码块
while(1){ // 子进程循环
memset(msg,0,sizeof(msg)); // 清空消息缓冲区
printf("input: "); // 提示用户输入
gets(msg); // 获取用户输入
write(c_fd,msg,strlen(msg)); // 将消息发送到服务器
}
}
// 父进程代码块:读取服务器响应
while(1){
memset(readBuf,0,sizeof(readBuf)); // 清空读取缓冲区
n_read = read(c_fd, readBuf, 128); // 从服务器读取数据
if(n_read == -1){ // 检查读取是否出错
perror("read"); // 输出读取错误信息
}else{
printf("\nget:%s\n",readBuf); // 输出从服务器接收到的数据
}
}
}
// 3.send(注释表明此处原本计划放置发送代码)
// 4.read(注释表明此处原本计划放置读取代码)
return 0; // 程序结束(实际上由于上面的无限循环,不会执行到这里)
}
重点
- socket函数 创建 socket 网络数据交换的门户
- bind函数 和socket关联起来
- 填充地址结构体
sockaddr_in
inet_addr("192.168.1.100")
:将点分十进制的IP字符串转换为网络字节序的32位整数。my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地IP
- 填充地址结构体
- listen函数 监听 将一个已绑定的 socket 从“主动”转换为“被动”模式,并通知操作系统开始监听来自客户端的连接请求
- accept函数 从已完成连接队列(Accept Queue)中取出第一个连接请求,为这个连接创建一个新的socket,并返回其文件描述符。
- inet_aton函数 将点分十进制格式的IPv4地址字符串
- inet_ntoa函数 将网络格式的ip地址转为字符串格式
- connect函数 主动与服务器建立连接
- gets 函数 标准输入
- sprintf函数 格式化数据写入字符串