Linux操作系统———TCP网络编程

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 服务器都遵循这个流程:

  1. socket() → 拿到一个"电话机"(文件描述符)

  2. bind() → 给电话机装上"固定号码"(IP + 端口)

  3. listen() → 把电话设为"待机接听"模式

  4. accept() → 等有人打进来,接起电话(得到新连接)

  5. send/recv → 开始通话

客户端则是:

  1. socket() → 拿电话机

  2. connect() → 拨号(连服务器)

  3. 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) 刷写所有输出流 ✅ 安全

在实际开发的时候呢,他除了有缓冲模式的介绍之外,他还会有个时间去控制它,你写了一段时间,既没有写满也没有换行符也不是用的无缓冲怎么办,加一个定时就可以了,当有数据写出的时候启动定时器,过了一段时间发现没有写出没有刷写,那就手动执行一下刷写就可以了

相关推荐
MediaTea8 小时前
Python:接口隔离原则(ISP)
开发语言·网络·python·接口隔离原则
XH-hui8 小时前
【打靶日记】HackMyVm 之 Listen
linux·网络安全·hackmyvm·hmv
FreeBuf_8 小时前
攻击者利用React2Shell漏洞部署Linux后门程序,日本成重点攻击目标
linux·运维·安全
I · T · LUCKYBOOM8 小时前
25.本地yum仓库搭建--CentOS 7
linux·运维·centos
拾忆,想起8 小时前
Dubbo RPC 实战全流程:从零搭建高可用微服务系统
网络·网络协议·微服务·性能优化·rpc·架构·dubbo
知识分享小能手8 小时前
CentOS Stream 9入门学习教程,从入门到精通,CentOS Stream 9 的过滤器 —— 语法详解与实战案例(18)
linux·学习·centos
AI视觉网奇8 小时前
Meta-Llama-3.1-8B-bnb-4bit 下载加载
linux·开发语言·python
colofullove8 小时前
计算机网络-5-网络层
网络·计算机网络
学习3人组9 小时前
docker运行报错启动守护进程
linux·运维·centos