Socket 是网络通信的端点,是应用程序访问网络协议栈的接口。在 Linux 中,它表现为一个文件描述符,通过 bind、listen、accept、connect、send、recv 等系统调用实现通信。它可以用于 TCP 或 UDP 通信,支持跨主机或本地进程通信。
套接字主要有三个属性组成:网络地址,端口号,协议
在网络通信中,为了让不同字节序的主机能够相互理解对方的数据,常常需要进行字节序转换。例如,发送数据前需要将主机字节序转换为网络字节序,接收数据后则需要将网络字节序转换为主机字节序。例如,如果你有一个IP地址或端口号(通常存储为整数)需要在网络上传输,就需要先使用htons()或htonl()将其转换为网络字节序,然后在网络另一端接收时,使用ntohs()或ntohl()将其转换回主机字节序。这样可以确保数据在网络中的传输不受不同主机字节序的影响。
小端序:低位字节存放在低地址。(即"小端在前")
大端序:高位字节存放在低地址。(即"大端在前")
例如:如果数值 0x1234 在内存中 低地址存 0x34,高地址存 0x12 → 小端序
如果数值 0x1234 在内存中 低地址存 0x12,高地址存 0x34 → 大端序
笔者用于测试的PC架构是x86-64,因此主机字节序为小端字节序。网络字节序为大端字节序。
Intel x86/x64(包括你的 Windows 电脑)是小端序
网络协议(TCP/IP)统一规定使用大端序,即网络字节
下面是一个简单的主机字节序与网络字节序互转的程序:
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char const *argv[])
{
//网络地址赋值
unsigned short local_num = 0x1F, network_num = 0;
network_num = htons(local_num);
printf("将主机字节序的无符号整数: 0x%hX 转换为网络字节序的结果为: 0x%hX\n", local_num, network_num);
local_num = ntohs(network_num);
printf("将网络字节序的无符号整数: 0x%hX 转换为主机字节序的结果为: 0x%hX\n", network_num, local_num);
return 0;
}
------------------------运行日志------------------------
将主机字节序的无符号整数: 0x1F 转换为网络字节序的结果为: 0x1F00
将网络字节序的无符号整数: 0x1F00 转换为主机字节序的结果为: 0x1F
这里有几个注意事项:htons / ntohs 专用于 16 位(short) 数据,如果是 32 位整数(如 IPv4 地址旧式表示),要用:htonl() / ntohl()(l 表示 long,32 位)。这两个函数 传值调用 ,不修改原变量。它们操作的是 整数值本身,不是内存地址。
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
int main(int argc, char const *argv[])
{
// 网络地址赋值
struct sockaddr_in server_addr;
struct in_addr server_in_addr;
in_addr_t server_in_addr_t;
memset(&server_addr, 0, sizeof(server_addr));
memset(&server_in_addr, 0, sizeof(server_in_addr));
memset(&server_in_addr_t, 0, sizeof(server_in_addr_t));
// 打印16进制IP地址作为对照
printf("192.168.6.101 的16进制表示为 0x%X 0x%X 0x%X 0x%X\n", 192, 168, 6, 101);
// 不推荐使用,因为输入-1返回的地址是一个有效地址(255.255.255.255)
server_in_addr_t = inet_addr("192.168.6.101");
printf("inet_addr convert: 0x%X\n", server_in_addr_t);
inet_aton("192.168.6.101", &server_in_addr);
printf("inet_aton convert: 0x%X\n", server_in_addr.s_addr);
// 推荐使用
// 字符串转sockin_addr结构体
inet_pton(AF_INET, "192.168.6.101", &server_addr.sin_addr);
printf("inet_pton 后 server_addr.sin_addr 的16进制表示为 0x%X\n", server_addr.sin_addr.s_addr);
// 结构体转化为字符串
printf("通过inet_ntoa打印inet_pton转化后的地址: %s\n", inet_ntoa(server_addr.sin_addr));
// 打印本地网络地址部分
printf("local net section: 0x%X\n", inet_lnaof(server_addr.sin_addr));
// 打印网络号部分
printf("netword number section: 0x%X\n", inet_netof(server_addr.sin_addr));
// 使用本地网络地址和网络号可以拼接成in_addr
server_addr.sin_addr = inet_makeaddr(inet_netof(server_addr.sin_addr), 102);
// 以网络字节序16进制打印拼接的地址
printf("inet_makeaddr: 0x%X\n", server_addr.sin_addr.s_addr);
// 打印拼接的地址
printf("通过inet_ntoa打印inet_makeaddr拼接后的地址%s\n", inet_ntoa(server_addr.sin_addr));
return 0;
}
------------------------运行日志------------------------
192.168.6.101 的16进制表示为 0xC0 0xA8 0x6 0x65
inet_addr convert: 0x6506A8C0
inet_aton convert: 0x6506A8C0
inet_pton 后 server_addr.sin_addr 的16进制表示为 0x6506A8C0
通过inet_ntoa打印inet_pton转化后的地址: 192.168.6.101
local net section: 0x65
netword number section: 0xC0A806
inet_makeaddr: 0x6606A8C0
通过inet_ntoa打印inet_makeaddr拼接后的地址192.168.6.102
对于上面的这些函数呢我们需要知道,ip地址在电脑里不是"192.168.6.101"这样的字符串,电脑网络通信时,必须用二进制,而且这个顺序还是有讲究的(网络规定用大端顺序),所以我们需要一些API来翻译成电脑能用的二进制。
1,inet_addr("192.168.6.101")这个函数输入对了会返回-1,但输入错误也会返回-1,这时我们就不知道它到底是输入对了还是错了,所以我们不推荐使用
2,inet_aton("192.168.6.101", &addr)这个函数成功返回1,失败返回0
3,inet_pton(AF_INET, "192.168.6.101", &addr)这个函数的第一个参数AF_INET代表IPv4,成功返回1,失败返回0
4,inet_ntoa(addr)这个函数呢就是再把电脑看得懂的二进制转换成我们更喜欢看到ip格式
------------------------single_conn_server.c------------------------
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
void *read_from_client(void *argv)
{
int client_fd = *(int *)argv;
char *read_buf = NULL;
ssize_t count = 0;
read_buf = malloc(sizeof(char) * 1024);
if (!read_buf)
{
perror("malloc server read_buf");
return NULL;
}
while ((count = recv(client_fd, read_buf, 1024, 0)))
{
if (count < 0)
{
perror("recv");
}
fputs(read_buf, stdout);
}
printf("客户端请求关闭连接......\n");
free(read_buf);
return NULL;
}
void *write_to_client(void *argv)
{
int client_fd = *(int *)argv;
char *write_buf = NULL;
ssize_t send_count = 0;
write_buf = malloc(sizeof(char) * 1024);
if (!write_buf)
{
printf("写缓存分配失败,断开连接\n");
shutdown(client_fd, SHUT_WR);
perror("malloc server write_buf");
return NULL;
}
while (fgets(write_buf, 1024, stdin) != NULL)
{
send_count = send(client_fd, write_buf, 1024, 0);
if (send_count < 0)
{
perror("send");
}
}
printf("接收到命令行的终止信号,不再写入,关闭连接......\n");
shutdown(client_fd, SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
int sockfd, temp_result, client_fd;
pthread_t pid_read, pid_write;
struct sockaddr_in server_addr, client_addr;
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 声明IPV4通信协议
server_addr.sin_family = AF_INET; //地址类型
// 我们需要绑定0.0.0.0地址,转换成网络字节序后完成设置
//绑定到 所有网卡 (0.0.0.0),意思是"谁都能连我"
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口随便用一个,但是不要用特权端口
server_addr.sin_port = htons(6666);
// 创建server socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sockfd);
// 绑定地址
temp_result = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("bind", temp_result);
// 进入监听模式
temp_result = listen(sockfd, 128); //最多排队 128 个未处理的连接
handle_error("listen", temp_result);
// 接受第一个client连接
socklen_t cliaddr_len = sizeof(client_addr);
//返回的文件描述符才是能够和客户端收发消息的文件描述
//如果调用accept之后没有客户端连接 这里会挂起等待
client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len);
handle_error("accept", client_fd);
//inet_ntoa :把客户 IP(二进制)转成 "192.168.6.100" 这样的字符串(用于打印)
//ntohs:把客户的端口(网络序)转回主机能看的数字(比如 54321)
printf("与客户端 from %s at PORT %d 文件描述符 %d 建立连接\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd);
// 启动一个子线程,用来读取客户端数据,并打印到 stdout
pthread_create(&pid_read, NULL, read_from_client, (void *)&client_fd);
// 启动一个子线程,用来从命令行读取数据并发送到客户端
pthread_create(&pid_write, NULL, write_to_client, (void *)&client_fd);
// 阻塞主线程
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
printf("释放资源\n");
close(client_fd);
close(sockfd);
return 0;
}
------------------------single_conn_client.c------------------------
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
// 192.168.6.101 IP 地址的16进制表示
#define INADDR_LOCAL 0xC0A80665
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
void *read_from_server(void *argv)
{
int sockfd = *(int *)argv;
char *read_buf = NULL;
ssize_t count = 0;
read_buf = malloc(sizeof(char) * 1024);
if (!read_buf)
{
perror("malloc client read_buf");
return NULL;
}
while (count = recv(sockfd, read_buf, 1024, 0))
{
if (count < 0)
{
perror("recv");
}
fputs(read_buf, stdout);
}
printf("收到服务端的终止信号......\n");
free(read_buf);
return NULL;
}
void *write_to_server(void *argv)
{
int sockfd = *(int *)argv;
char *write_buf = NULL;
ssize_t send_count = 0;
write_buf = malloc(sizeof(char) * 1024);
if (!write_buf)
{
printf("写缓存分配失败,断开连接\n");
shutdown(sockfd, SHUT_WR);
perror("malloc client write_buf");
return NULL;
}
while (fgets(write_buf, 1024, stdin) != NULL)
{
send_count = send(sockfd, write_buf, 1024, 0);
if (send_count < 0)
{
perror("send");
}
}
printf("接收到命令行的终止信号,不再写入,关闭连接......\n");
shutdown(sockfd, SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
int sockfd, temp_result;
pthread_t pid_read, pid_write;
struct sockaddr_in server_addr, client_addr;
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
server_addr.sin_family = AF_INET;
// 连接本机 127.0.0.1
server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
// 连接端口 6666
server_addr.sin_port = htons(6666);
client_addr.sin_family = AF_INET; //地址类型
// 连接本机 192.168.10.150
client_addr.sin_addr.s_addr = htonl(INADDR_LOCAL);
// 连接端口 8888
client_addr.sin_port = htons(8888);
// 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sockfd);
temp_result = bind(sockfd, (struct sockaddr *)&client_addr, sizeof(client_addr));
handle_error("bind", temp_result);
// 连接server
temp_result = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("connect", temp_result);
// 启动一个子线程,用来读取服务端数据,并打印到 stdout
pthread_create(&pid_read, NULL, read_from_server, (void *)&sockfd);
// 启动一个子线程,用来从命令行读取数据并发送到服务端
pthread_create(&pid_write, NULL, write_to_server, (void *)&sockfd);
// 阻塞主线程
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
printf("关闭资源\n");
close(sockfd);
return 0;
}
struct sockaddr_in {
short sin_family; // 地址类型(必须是 AF_INET)
unsigned short sin_port; // 端口号(网络字节序!)
struct in_addr sin_addr; // IP 地址(网络字节序!)
char sin_zero[8]; // 填充字段(不用管,设为0)
};
struct in_addr {
in_addr_t s_addr; // 一个 32 位无符号整数
};
ntohs(client_addr.sin_port);把网络字节序的端口转回主机能看到的数字
client_addr.sin_port → 这是一个 unsigned short(网络序)
shutdown(fd, SHUT_RD):不再读 (关闭读端)
shutdown(fd, SHUT_WR):不再写 (关闭写端)
shutdown(fd, SHUT_RDWR):读写都关
我们在服务器程序中:创建了一个监听socket,告诉系统我要在本机的某个ip和端口上等别人来连我,所以我们设置
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本机所有 IP
server_addr.sin_port = htons(6666); // 本机端口 6666
我们在客户端程序中:创建了一个连接socket,要告诉系统两件事:
我要连谁(目标服务器地址)
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 目标 IP: 127.0.0.1
server_addr.sin_port = htons(6666); // 目标端口: 6666
我自己用哪个地址发(可选,本地绑定地址)
client_addr.sin_family = AF_INET;
client_addr.sin_addr.s_addr = htonl(INADDR_LOCAL); // 本机 IP: 192.168.10.150
client_addr.sin_port = htons(8888); // 本机端口: 8888
注意:客户端通常不需要设置 client_addr!
如果你省略 bind(),系统会自动选一个临时端口(比如 54321)和合适的本机 IP
bind() 是"我占用哪个 (IP,端口)",
connect() 是"我连哪个 (IP,端口)"。
服务器 bind(0.0.0.0:6666) 会占用 所有本地IP的 6666,
所以客户端不能再 bind 到 任何本地IP的 6666,
但客户端 connect 到 6666 是完全 OK 的!
你运行服务器:
bind(0.0.0.0:6666) → 监听所有 IP 的 6666 端口
你运行客户端:
bind(192.168.10.150:8888)(可选)→ 强制用这个地址发
connect(127.0.0.1:6666) → 连本机的 6666 端口
服务器的accept()返回:
client_addr.sin_addr = 127.0.0.1(因为连的是回环)
client_addr.sin_port = 8888(如果你 bind 了)或随机端口(如 54321)
服务器打印:
与客户端 from 127.0.0.1 at PORT 8888 ...
所有 TCP 服务器都遵循这个流程:
-
socket() → 拿到一个"电话机"(文件描述符)
-
bind() → 给电话机装上"固定号码"(IP + 端口)
-
listen() → 把电话设为"待机接听"模式
-
accept() → 等有人打进来,接起电话(得到新连接)
-
send/recv → 开始通话
客户端则是:
-
socket() → 拿电话机
-
connect() → 拨号(连服务器)
-
send/recv → 通话
创建连接的方法介绍就是先创建一个socket,然后绑定到对应的当前的地址,绑定后进入监听状态等待客户端连接,客户端调了connect之后呢,你可以调accept去接收到这个连接,然后返回一个特定的套接字,这个套接字从才能跟客户端进行收发消息
接下来我们再说一下缓冲区知识的补充,大约有三种,无缓冲,全缓冲,行缓冲
无缓冲就比如你喝果汁,每喝一口,果汁就立即到胃里
全缓冲就比如你喝完之后不咽下去一直含着,等含满了再一口咽下去
行缓冲就是你每次都固定比如说每喝三分之一就咽下去一次
在电脑中你这个含在嘴里就相当于一个缓冲区(buffer)
程序要输出数据(比如说printf),不是立刻发送到屏幕,而是先存到缓冲区,等慢了或满足条件再一次性输出出来
那为什么要有缓冲区呢?就好比你去跑一千次接一小杯水跟直接接一大桶水,为的就是提高小区和减少系统调用,
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
第一个参数是填标准输入输出错误或者文件指针(比如fopen返回的)
第二个参数是你自己提供一块内存当缓冲区,填NULL系统会自动分配
第三个参数代表模式
| 模式 | 含义 | 适用场景 |
|---|---|---|
_IONBF |
无缓冲 (Unbuffered)<br>→ 每次 printf 立刻刷到屏幕 |
调试、实时日志 |
_IOLBF |
行缓冲 (Line buffered)<br>→ 遇到 \n 就刷 |
终端输出(stdout 默认) |
_IOFBF |
全缓冲(Fully buffered)<br>→ 缓冲区满了才刷 | 文件输出(fprintf 到文件) |
默认情况:
stdout到终端 :行缓冲(有\n就刷)stdout到文件:全缓冲stderr:总是无缓冲(所以错误信息立刻出现)
第四个参数是缓冲区多大(单位是字节),比如你第二个参数传了个buffer,那你这个参数就可以写个sizeof(buffer),如果是NULL就写0
int fflush(FILE *stream);这个函数可以用来手动刷写
| 调用 | 作用 | 是否安全 |
|---|---|---|
fflush(stdout) |
立刻显示 printf 内容 |
✅ 安全 |
fflush(stderr) |
立刻显示错误信息 | ✅ 安全(但通常没必要) |
fflush(fp) |
立刻把数据写进文件 | ✅ 安全 |
fflush(NULL) |
刷写所有输出流 | ✅ 安全 |
在实际开发的时候呢,他除了有缓冲模式的介绍之外,他还会有个时间去控制它,你写了一段时间,既没有写满也没有换行符也不是用的无缓冲怎么办,加一个定时就可以了,当有数据写出的时候启动定时器,过了一段时间发现没有写出没有刷写,那就手动执行一下刷写就可以了