以下为一个服务端构建 socket 套接字需要做到的完整四个步骤
对于需要连接上服务端的客户端而言,相对于的代码就简洁了很多
仅需先调用 socket 函数创建套接字,之后再调用 connect 函数向服务器端发送链接请求
文件描述符(file descriptor)是一个整数值,用于表示操作系统中的打开文件或者输入/输出设备。它是对打开文件或设备的引用,通过文件描述符可以进行读取、写入和其他操作。
文件和套接字一般需要经过创建过程才会被分配文件描述符
在 UNIX 系统中,对应的输入输出三个文件描述符是:
标准输入(stdin)、标准输出(stdout)和标准错误(stderr)分别有文件描述符 0、1 和 2
由于 Linux 不区分文件以及套接字,可以直接使用对应的文件处理函数来处理套接字
比如 open、close、write
常见的以 _t
结尾的数据类型:
size_t
: 这是一个无符号整数类型,在 头文件中定义。它用于表示对象大小或容器的大小。ptrdiff_t
: 这是一个有符号整数类型,在 头文件中定义。它用于表示指针之间的差异(偏移量)。time_t
: 这是一个整数类型,在 头文件中定义。它用于表示从特定时间点(通常是 1970 年 1 月 1 日)起经过的秒数。int32_t, int64_t, uint32_t, uint64_t
: 这些是固定宽度的整数类型,在 头文件中定义。它们分别表示有符号的 32 位整数、有符号的 64 位整数、无符号的 32 位整数和无符号的 64 位整数。这是一个简单的,使用文件操作的小案例
#include // 标准输入输出操作
#include // 文件控制选项
#include // 文件操作
int main(void) {
int fd; // 文件描述符变量
char buf[] = "helloworld\n"; // 要写入的数据缓冲区
fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC); // 打开文件以进行写入操作
if (fd == -1) {
// 如果文件打开失败,进行错误处理
std::cerr << "无法打开文件。" << std::endl;
return 1; // 返回非零值表示错误
}
std::cout << "文件描述符:" << fd << std::endl; // 打印文件描述符
if (write(fd, buf, sizeof(buf)) == -1) {
// 如果写入操作失败,进行错误处理
std::cerr << "写入文件失败。" << std::endl;
close(fd); // 在返回前关闭文件
return 1; // 返回非零值表示错误
}
close(fd); // 关闭文件
return 0; // 返回0表示成功执行
}
以下代码示例,创建文件和套接字,使用整数返回文件描述符值
#include // 标准输入输出操作
#include // socket 函数相关类型
#include // socket 函数
#include // 文件控制选项
#include // 文件操作
int main(void) {
int fd1, fd2, fd3;
fd1 = socket(PF_INET, SOCK_STREAM, 0); // 创建 TCP 套接字
fd2 = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC); // 打开文件进行写入操作
fd3 = socket(PF_INET, SOCK_DGRAM, 0); // 创建 UDP 套接字
close(fd1); // 关闭套接字
close(fd2); // 关闭文件
close(fd3); // 关闭套接字
return 0; // 返回0表示成功执行
}
windows 下的句柄相当于 linux 的文件描述符
但不同的是 windows 还包括了文件句柄和套接字句柄
由于 windows 下的 CPP 开发不切实际,所以这里就不多陈述了,大家可以查阅相关的资料补全这部分内容,如果大家感兴趣的话!
接下来将会把全本的笔记主要集中在 linux 开发上
如何创建套接字?
int socket(int domain, int type, int protocol); // domain:采取的协议族,一般为 PF_INET;type:数据传输方式,一般为 SOCK_STREAM;protocol:使用的协议,一般设为 0 即可。
//成功时返回文件描述符,失败时返回 -1
创建套接字的函数 socket 的三个参数的含义:
domain
:使用的协议族。一般只会用到 PF*INET,即 IPv4 协议族。type
:套接字类型,即套接字的数据传输方式。主要是两种:SOCK_STREAM(即 TCP)和 SOCK*(即 UDP)。protocol
:选择的协议。一般情况前两个参数确定后,protocol 也就确定了,所以设为 0 即可。重点关注 IPV4 对应的 PF_INET
协议族,绝大部分情况下使用它,即便目前 IPV6 正在推广
SOCK_STREAM
面向链接的套接字,代表 TCP 协议
int tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
SOCK_DGRAM
面向消息的套接字,代表 UDP 协议
int udp_socket = socket(PF_INET, SOCK_DGRAM, 0);
当同一协议族中存在多个数据传输方式相同的协议时,需要使用第三个参数来明确指定协议信息
IPV4中面向链接的套接字
这么写
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
IPV4中面向消息的套接字
这么写
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_UDP);
服务端程序 tcp_server.c
#include // 标准输入输出
#include // 标准库函数
#include // 字符串操作
#include // UNIX 标准函数
#include // 网络地址转换函数
#include // 套接字函数
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock; // 服务器套接字
int clnt_sock; // 客户端套接字
struct sockaddr_in serv_addr; // 服务器地址结构体,用于持续监听连接请求
struct sockaddr_in clnt_addr; // 客户端地址结构体,用于与客户端连接以传输数据
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if (argc != 2) // 需要两个参数:可执行文件名、端口号
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0); // 创建一个 TCP 套接字
if (serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr)); // 将 serv_addr 结构体全部置为 0,主要是为了将 serv_addr 的 sin_zero 成员设为 0
serv_addr.sin_family = AF_INET; // 选择 IPv4 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl:将 long 类型数据从主机字节序转换为网络字节序;INADDR_ANY:表示接受任意 IP 地址
serv_addr.sin_port = htons(atoi(argv[1])); // 此程序运行时应在可执行文件名后跟一个端口号作为参数,例如:hello_server 3030
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 将套接字与服务器的 IP 地址和端口号绑定
error_handling("bind() error");
if (listen(serv_sock, 5) == -1) // 将套接字转换为监听状态,最多允许同时连接 5 个客户端
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); // 接收一个连接请求,并将 clnt_sock 套接字与其连接
if (clnt_sock == -1)
error_handling("accept() error");
write(clnt_sock, message, sizeof(message)); // 向客户端发送信息。注意:clnt_sock 不是客户端的套接字,而是服务器上与客户端连接的套接字
close(clnt_sock); // 关闭与客户连接的套接字,断开该连接
close(serv_sock); // 关闭监听端口的套接字,不再接受任何请求
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端 TCP 套接字程序 tcp_client.c
#include // 标准输入输出
#include // 标准库函数
#include // 字符串操作
#include // UNIX 标准函数
#include // 网络地址转换函数
#include // 套接字函数
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock; // 客户端套接字
struct sockaddr_in serv_addr; // 服务器地址结构体
char message[30]; // 存储接收到的消息
int str_len; // 读取的消息长度
if (argc != 3)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0); // 创建一个 TCP 套接字
if (sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr)); // 将 serv_addr 结构体全部置为 0
serv_addr.sin_family = AF_INET; // 选择 IPv4 地址族
serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // 设置服务器 IP 地址
serv_addr.sin_port = htons(atoi(argv[2])); // 设置服务器端口号
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 连接到服务器
error_handling("connect() error");
str_len = 0;
int idx = 0, read_len = 0;
while (read_len = read(sock, &message[idx++], 1)) // 只有当 read 函数读到 EOF (即服务器端调用了 close 函数) 才会中止循环
{
if (read_len == -1)
error_handling("read() error");
str_len += read_len; // 统计读取的消息长度
}
printf("Message from server: %s\n", message);
printf("Function read call count: %d\n", str_len);
close(sock); // 关闭套接字
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}