IPV4
属 4 字节地址族
IPV6
属 16 字节地址族
A 类地址首字节范围:0-127
B 类地址首字节范围:128-191
C 类地址首字节范围:192-223
计算机内部的网络接口卡(NIC)传输数据时,会附带一个端口号,计算机根据此端口号传递到对应套接字里面
端口号特点
该结构体定义了套接字地址的格式,用于在套接字编程中指定服务器和客户端的网络地址
struct sockaddr_in {
sa_family_t sin_family; // 地址族,通常为 AF_INET(IPv4);AF_INET6(IPV6)
uint16_t sin_port; // 16 位端口号,使用网络字节序(大端序)
struct in_addr sin_addr; // 表示 32 位 IP 地址的结构体
char sin_zero[8]; // 不使用,填充字节,通常为全零
};
该结构体为通用型,不仅仅用于 IPV4
struct sockaddr {
sa_family_t sin_family; // 地址族,用于指定地址的类型
char sa_data[14]; // 地址信息,具体格式取决于地址族的类型
};
CPU 向内存保存数据的方式有两种:
数据数组首先转换成统一的大端序格式进行网络传输——网络字节序
小端序系统传输数据时应当转换为大端序格式
接收数据是以小端存储的
这是几个景点的转换字节序的函数:htons、ntohs、htonl、ntohl
现在对 htons 分析:
仅有向 sockaddr_in 结构体变量填充数据之外,其余情况不需考虑字节序问题
我们需要把我们经常看到的 IP 地址形式(点分十进制)改为 sockaddr_in 所接受的 32 位整型数据
下面有两种函数可以实现此功能
转换类型附带网络字节序转换,还可以识别错误的 IP 地址
#include
in_addr_t inet_addr(const char* string);
他利用了 inet_addr
结构体,效率更高
他接受一个 inet_addr
指针,运行完毕后把数据原路保存到该指针
#include
int inet_aton(const char* string, struct in_addr* addr);
顾名思义,则可以得到 inet_ntoa 就是一个反向转换
struct sockaddr_in addr; // 声明一个 sockaddr_in 结构体变量 addr
char *serv_ip = "211.217.168.13"; // 声明 IP 地址字符串
char *serv_port = "9190"; // 声明端口号字符串
memset(&addr, 0, sizeof(addr)); // 将 addr 的所有成员初始化为 0,主要是为了将 sockaddr_in 的成员 sin_zero 初始化为 0。
addr.sin_family = AF_INET; // 指定地址族为 AF_INET,表示 IPv4 地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 使用 inet_addr 函数将字符串形式的 IP 地址转换为二进制形式,并将结果存储在 sin_addr.s_addr 中
addr.sin_port = htons(atoi(serv_port)); // 使用 atoi 函数将端口号字符串转换为整数,并使用 htons 函数将端口号转换为网络字节序(大端序),然后存储在 sin_port 中
当然,你要是嫌麻烦的话,可以避免每次都输入 IP 地址,改为使用 INADDR_ANY
作为替代品
使用此方法,可以自动获取运行服务器端的主机 IP 地址(服务端优先采用此方法)
此部分可以参阅计算机网络对应的总结笔记,或者相应面经,在此处不做具体介绍
下面即为对应的默认函数调用顺序
在调用 accept 函数前,请求会处于等待状态
服务器端处于等待连接请求状态指的是:让来自客户端的请求处于等待状态,以等待服务器端受理它们的请求。
链接等待队列:未被受理的连接请求被放置于此处,该连接池的大小取决于 backlog 定义的大小
#include
// 返回值:成功时返回 0,失败时返回 -1
// 参数一:传递文件描述符套接字的用途
// 参数二:等待队列的最大长度
int listen(int sockfd, int backlog);
#include
// sockfd:服务器套接字的文件描述符;sockaddr:用于保存发起连接请求的客户端地址信息;addrlen:第二个参数的长度。
// 返回值:成功时返回创建的套接字文件描述符,失败时返回 -1
int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);
服务器套接字只是控制是否允许连接请求进入服务器端
accept
函数会受理连接请求等待队列中待处理的客户端连接请求,它从等待队列中取出 1 个连接请求,创建套接字并完成连接请求。如果等待队列为空,accpet 函数会阻塞,直到队列中出现新的连接请求才会返回。
accept 执行完毕后会将它所受理的连接请求对应的客户端地址信息存储到第二个参数 addr 中。
accept 函数调用成功后,其会在内部产生一个新的套接字并返回其文件描述符,该套接字用于与客户端建立连接并进行数据 I/O。
客户端默认函数调用顺序为
#include
// 参数:sock:客户端套接字的文件描述符;serv_addr:保存目标服务器端地址信息的结构体指针;addrlen:第二个参数的长度(单位是字节)
// 返回值:成功时返回 0,失败时返回 -1
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
客户端调用 connect
函数后会阻塞,直到发生以下情况之一才会返回:
注意:connect 函数返回后并不立即进行数据交换
客户端只有等到服务器端调用 listen
函数后才能调用 connect
函数
重复调用 accept 函数即可实现客户端链接请求的持续受理
目前我们仅能实现:受理完一个客户端后在受理下一个,无法同时受理多个客户端(这一技巧将在后续展开介绍)
基本运行方式:
一般的,服务器端不可能提前知道客户端发来的数据有多长,所以只能通过应用层协议确定数据边界以及对应长度大小
应用层协议实际就是在服务器端/客户端的实现过程中逐步定义的规则的集合。
下面依次展示了计数器服务器端和计数器客户端的代码
// 客户端代码
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 50
#define OPSZ 4 // 定义每个操作数在 TCP 报文中占用的字节数
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char opmsg[BUF_SIZE]; // opmsg 用来存储要发送的数据,注意是 char 类型数组
struct sockaddr_in serv_addr;
int operand_count, result;
if (argc != 3)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)))
error_handling("connect() error");
else
puts("Connecting..........\n");
fputs("Operand count: ", stdout);
scanf("%d", &operand_count);
opmsg[0] = (char)operand_count; // 数据的第一个字节存储操作数的数量,注意要将变量类型转换为 char。
for (int i = 0; i < operand_count; i++)
{
printf("Operand %d: ", i + 1);
scanf("%d", (int *)&opmsg[i * OPSZ + 1]); // 从第二个字节开始每四个字节存储一个操作数,向数组存数据时先取地址再转换类型。
}
fgetc(stdin);
fputs("Operator: ", stdout);
scanf("%c", &opmsg[operand_count * OPSZ + 1]); // 再用一个字节存储运算符
write(sock, opmsg, operand_count * OPSZ + 2); // 发送数据
read(sock, &result, OPSZ); // 接收运算结果:运算结果是一个 4 字节的操作数
printf("Operation result: %d\n", result);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
// 服务端代码
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 50
#define OPSZ 4
void error_handling(char *message);
int calculate(int operand_count, int operands[], char operator);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
int clnt_addr_sz;
char message[BUF_SIZE];
if (argc != 2)
{
printf("Usage : %s " , argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_sz = sizeof(clnt_addr);
for (int i = 0; i < 5; i++)
{
if ((clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_sz)) == -1)
error_handling("accept() error");
int operand_count;
read(clnt_sock, &operand_count, 1); // 首先读取第 1 个字节,获取操作数的数量
char operands[BUF_SIZE];
for (int i = 0; i < operand_count; i++)
{
read(clnt_sock, &operands[i * OPSZ], OPSZ); // 根据操作数数量,依次读取操作数
}
char operator;
read(clnt_sock, &operator, 1); // 读取运算符
int result = calculate(operand_count, (int *)operands, operator);
write(clnt_sock, (char *)&result, sizeof(result)); // 发送计算结果
close(clnt_sock);
}
close(serv_sock);
return 0;
}
int calculate(int operand_count, int operands[], char operator)
{
int result = operands[0];
switch (operator)
{
case '+':
for (int i = 1; i < operand_count; i++)
result += operands[i];
break;
case '-':
for (int i = 1; i < operand_count; i++)
result -= operands[i];
break;
case '*':
for (int i = 1; i < operand_count; i++)
result *= operands[i];
break;
}
return result;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
调用 write 函数,数据先移入到输出缓冲,之后选择合适时间传输到对方输入缓冲内;
调用 read 函数,读取输入缓冲中的内容;
套接字对应的 IO 特性