Socket(套接字)是计算机网络编程中的一种抽象,用于在不同计算机之间的进程之间进行通信。它是网络通信的基本构建块,允许进程通过网络传输数据。Socket 用于在网络上标识一个通信端点,这个端点通常由 IP 地址和端口号组成,它可以唯一标识网络上的一个特定应用程序或进程。Socket 存在多种类型,其中包括流式套接字(Stream Sockets),用于可靠的、基于流的通信,如TCP协议。还有数据报套接字(Datagram Sockets),用于不可靠的、面向消息的通信,如UDP协议。
进行网络通信的一般流程为:
创建和绑定:在编程中,你可以创建 Socket 对象,并将其绑定到特定的 IP 地址和端口。这使得其他设备可以通过网络连接到该 Socket。
连接:客户端套接字可以连接到服务器套接字,建立网络连接。这是通过套接字编程中的 connect 函数来实现的。
监听和接受:服务器套接字可以监听来自客户端的连接请求,并接受这些连接。这是通过套接字编程中的 listen 和 accept 函数来实现的。
发送和接收数据:Socket 可用于发送和接收数据。对于流式套接字,数据是连续的流,而对于数据报套接字,数据是分散的消息。
关闭:当通信完成时,套接字需要关闭以释放资源。
Socket 编程通常在不同编程语言中支持,并用于创建各种网络应用程序,包括网络服务器、客户端、P2P应用程序和网络通信协议的实现。
socket() 函数是在Linux和类Unix系统中用于创建套接字的系统调用,该套接字可以用于网络通信。函数原型如下:
其中参数 domain 为指定套接字的协议族,如 AF_INET(IPv4)或 AF_INET6(IPv6)用于Internet协议,AF_UNIX 或 AF_LOCAL 用于Unix域套接字等。type 参数为指定套接字的类型,可以是 SOCK_STREAM(面向连接的套接字,如TCP)或 SOCK_DGRAM(无连接的套接字,如UDP)等。protocol 参数是指定套接字所使用的具体协议,通常为0,表示默认协议。
socket() 调用成功时返回一个新的文件描述符,该描述符用于后续的套接字操作。如果调用失败,它将返回 -1,并设置全局变量 errno 以指示错误的类型。
示例:socket() 函数创建一个套接字,指定地址族(AF_INET 表示IPv4)和套接字类型(SOCK_STREAM 表示TCP套接字)。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("Socket creation failed");
exit(1);
}
在网络编程中用于将套接字与特定的IP地址和端口号关联可以使用 bind() 函数。它通常用于服务器端,让服务器可以监听特定的IP地址和端口,以接受客户端的连接请求。该函数原型如下:
sockfd 参数是之前使用 socket() 函数创建的套接字的文件描述符。addr 参数是一个指向 sockaddr 结构的指针,其中包含要绑定的IP地址和端口信息,addrlen 参数为指定 addr 结构的大小。bind() 函数在成功绑定套接字到指定的地址和端口时返回0,失败时返回-1,并设置 errno 来指示错误的类型。
示例:使用 struct sockaddr_in 结构体来配置套接字的地址信息,包括地址族、端口号以及IP地址。
// 绑定端口和地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Binding failed");
exit(1);
}
listen() 函数是在网络编程中用于将套接字设置为侦听连接请求状态的系统调用。通常,这仅适用于服务器端套接字,以便它可以接受来自客户端的连接请求。如下是 listen() 函数的原型:
其中参数 sockfd 是之前使用 bind() 函数绑定的套接字的文件描述符。backlog 参数为指定在队列中等待连接的客户端连接请求的最大数量。listen() 函数成功时返回0,失败时返回-1,并设置 errno 来指示错误的类型。一旦套接字通过 listen() 函数设置为侦听状态,它就可以接受来自客户端的连接请求。这通常在服务器端的主循环中执行,以处理多个客户端连接请求。
示例:将套接字设置为监听模式,等待客户端连接。一旦有客户端连接请求到来,accept 函数将返回一个新的套接字,该套接字可用于与客户端通信。在这里,我们简单地打印出了客户端已连接的消息。
// 打开监听模式
if (listen(sockfd, 5) == -1) {
perror("Listening failed");
exit(1);
}
printf("Client connected\n");
服务器端接收请求用的是 accept() 函数,该是一个系统调用,通常与套接字编程一起使用,特别是在 TCP 服务器中。它用于从已完成连接的队列中接受一个连接请求,然后返回一个新的套接字文件描述符,代表接受的连接。此新描述符与原始监听套接字分开。当客户端向服务器发送连接请求并且服务器通过 listen() 系统调用准备好接受连接时,此连接请求被放在已完成连接的队列中。然后服务器可以使用 accept() 来处理此请求。函数的基本原型如下:
其中参数 sockfd 是原始监听套接字的描述符。addr 是一个指向 sockaddr 的结构指针,该结构将被填充与接受的连接相关的客户端信息(如 IP 地址和端口号)。addrlen 参数是一个输入/输出参数表示该结构体的长度,在调用 accept() 之前,应将其设置为 addr 结构的大小。当 accept() 返回时,它将被设置为实际填充到 addr 中的数据的大小。如果 accept() 成功,它返回一个新的文件描述符。如果失败,它返回 -1,并设置适当的错误代码。
在 TCP 服务器中,有两个重要的队列,分别是已完成连接队列和未完成连接队列,当客户端向服务器发送连接请求时,服务器会将这些请求放入已完成连接队列中,但尚未接受它们。这些请求等待服务器使用 accept() 函数来接受它们。而未完成连接队列是正在进行连接握手的客户端连接的队列。这是一个临时队列,用于处理正在建立连接的请求。当客户端发送连接请求并且服务器已经通过 listen() 准备好接受连接时,连接请求将从未完成连接队列移动到已完成连接队列。
此时,服务器可以使用 accept() 函数来从已完成连接队列中接受一个连接请求。它会创建一个新的套接字文件描述符,代表这个新连接,然后你可以使用这个套接字来与客户端进行通信。
示例:如果accept函数成功接受了客户端连接,会输出"Client connected"表示客户端已连接到服务器。
// 接受连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_sock == -1) {
perror("Accepting connection failed");
exit(1);
}
printf("Client connected\n");
将客户端套接字连接到指定的服务器地址和端口使用的是 connect() 函数,这个函数也是一个系统调用,通常在套接字编程中用于客户端建立与服务器的连接。函数原型如下:
参数 sockfd 是客户端套接字的文件描述符。addr 是指向包含服务器地址信息的 sockaddr 结构的指针。addrlen 参数是 addr 结构的大小,一般使用 connect() 函数的流程如下:
如果 connect() 成功,它会返回 0。如果失败,它会返回 -1,并设置适当的错误代码。通常,connect() 用于建立客户端与服务器之间的网络连接,这允许客户端与服务器进行数据交换,例如获取网页、下载文件或进行其他网络通信操作。
示例:设置服务器地址和端口,这里为本主机,因此使用本地环回地址。
// 设置服务器地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// 连接到服务器
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Connection failed");
exit(1);
}
printf("Connected to server\n");
sockaddr 是一个通用的套接字地址结构,用于在套接字编程中表示和存储网络地址信息。它是一个抽象的结构,具体形式取决于套接字的协议族(例如,IPv4、IPv6、Unix 域套接字等),sockaddr 结构通常是作为一个基本结构,它的具体类型和字段根据所用的协议而有所不同。
sockaddr 的变种有 sockaddr_in 和 sockaddr_un 等,其中 sockaddr 用于表示不特定的协议。它包括两个字段,分别为 sa_family 和 sa_data。sa_family 是一个地址族字段,指示套接字的地址类型,例如 AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址,AF_UNIX 表示 Unix 域套接字等。sa_data 是一个包含地址信息的字节数组。
sockaddr_in 结构用于表示 IPv4 套接字地址信息,它包含 sin_family(地址族,通常设置为 AF_INET),sin_port 是一个 16 位整数,用于指定套接字通信的目标端口号。它确定了服务器上要连接的特定服务。sin_addr 是一个 struct in_addr 结构,用于表示 IPv4 地址。它包含了要连接的服务器的 IP 地址。sin_zero是一个填充字段,通常不使用。它的存在是为了与较早的套接字实现兼容。
sockaddr_in 结构用于在客户端和服务器端之间指定 IPv4 地址和端口号,以建立网络连接。它通常用于套接字编程中,例如在使用 TCP 或 UDP 协议进行网络通信时,以确定通信的目标服务器地址。
除了IPv4之外,还有用于表示 IPv6 套接字地址的结构,sockaddr_in6 就是用于表示 IPv6 套接字地址的结构。它是在IPv6网络编程中用于指定套接字通信的目标服务器地址的重要结构。
其中字段 sin6_family 通常设置为 AF_INET6,表示使用 IPv6 地址。sin6_port 是一个 16 位整数,用于指定套接字通信的目标端口号。它确定了服务器上要连接的特定服务。sin6_flowinfo是一个 32 位整数,通常不使用,用于指定数据流的信息。sin6_addr 是一个 struct in6_addr 结构,用于表示IPv6地址。sin6_scope_id 是一个 32 位整数,通常不使用,用于指定作用域 ID。sockaddr_in6 结构用于在客户端和服务器端之间指定IPv6地址和端口号,以建立网络连接。它通常用于套接字编程中,例如在使用TCP或UDP协议进行IPv6网络通信时,以确定通信的目标服务器地址。IPv6是IPv4的下一代互联网协议,提供更大的地址空间和其他改进,以应对日益增长的网络需求。
in_addr 是用于表示IPv4地址的结构,通常用于套接字编程和网络编程。
s_addr是一个32位无符号整数,用于存储IPv4地址。IPv4地址由四个8位组成,通常以点分十进制表示,例如 “192.168.0.1”。in_addr 结构主要用于 sockaddr_in 结构中,以表示IPv4套接字地址。在套接字编程中,你会使用 in_addr 结构来存储和处理IPv4地址,然后将其嵌套在 sockaddr_in 结构中,以指定套接字通信的目标服务器的IPv4地址。
sockaddr_un 是用于 Unix 域套接字(Unix Domain Socket)的套接字地址结构。这个结构用于在本地主机内的进程之间进行通信,而不依赖于网络协议。
sun_family 通常设置为 AF_UNIX,表示使用 Unix 域套接字。sun_path是一个以 null 结尾的字符串,用于指定套接字的文件系统路径。不同于网络套接字的 IP 地址和端口号,Unix 域套接字的地址是一个文件系统路径。sockaddr_un 允许不同的进程在同一台计算机上通过文件系统路径来识别和连接到彼此。这在本地进程间通信(IPC)中非常有用,因为它提供了一种机制,允许进程在同一台计算机上进行通信,而不受网络协议的限制。
我们在进行网络编程时,通常用点分十进制的字符串表示IP地址,因此涉及到一些字符串表示和in_addr表示之间转换。
进行字符串转in_addr的函数可以使用 inet_aton、inet_pton 和 inet_addr 函数,这些函数都用于将不同格式的IP地址字符串转换为二进制形式,以便在网络编程中进行处理。
其中 inet_aton 函数是用于将IPv4地址的字符串表示形式转换为32位二进制形式的IPv4地址的函数,它接受两个参数,一个指向包含IPv4地址的字符串的指针和一个指向 struct in_addr 结构体的指针。如果成功解析字符串,它将IPv4地址的二进制表示存储在 struct in_addr 结构体中,并返回1(成功);否则返回0(失败)。这个函数适用于IPv4地址转换,但不支持IPv6。它在处理特殊情况和错误时的灵活性有限,因此在处理IPv6或需要更多错误处理的情况下可能不是最佳选择。
示例:
#include
#include
#include
#include
int main() {
const char* ip_address_str = "192.168.1.1"; // 一个IPv4的点分十进制字符串
struct in_addr ip_address; // 用于存储转换后的IPv4地址
// 使用inet_aton将点分十进制字符串转换为IPv4地址结构
if (inet_aton(ip_address_str, &ip_address) == 0) {
perror("Invalid IP address");
exit(1);
}
// 打印转换后的IPv4地址
printf("IPv4 Address in binary: %u\n", ip_address.s_addr);
return 0;
}
inet_pton 用于将IPv4或IPv6地址的字符串表示形式转换为二进制形式的IP地址。它接受三个参数,分别为地址族(AF_INET 表示IPv4,AF_INET6 表示IPv6),一个指向包含IP地址的字符串的指针,和一个指向目标存储二进制形式地址的内存的指针。如果成功解析字符串,它将IP地址的二进制表示存储在提供的内存中,成功返回1,失败返回0。还可以结合使用 errno 来获取错误信息。这个函数支持IPv4和IPv6地址的转换,因此更通用,提供了更多的错误处理和对特殊情况的支持。
示例:
#include
#include
#include
#include
int main() {
const char* ip_address_str = "192.168.1.1"; // 一个IPv4的点分十进制字符串
struct in_addr ipv4_address; // 用于存储转换后的IPv4地址
// 使用inet_pton将点分十进制字符串转换为IPv4地址
if (inet_pton(AF_INET, ip_address_str, &ipv4_address) == 0) {
perror("Invalid IP address");
exit(1);
}
// 打印IPv4地址的二进制表示形式
printf("IPv4 Address in binary: %u\n", ipv4_address.s_addr);
return 0;
}
inet_addr 用于将IPv4地址的字符串表示形式转换为32位二进制形式的IPv4地址。它接受一个指向包含IPv4地址的字符串的指针。如果成功解析字符串,它返回32位二进制形式的IPv4地址,或者返回特殊值 INADDR_NONE,通常是-1,表示错误。这个函数不适用于IPv6地址转换,且在处理特殊情况方面不够灵活。它已被标记为过时,不建议在新代码中使用。
示例:
#include
#include
#include
#include
int main() {
const char* ip_address_str = "192.168.1.1"; // 一个IPv4的点分十进制字符串
unsigned int ipv4_address; // 用于存储转换后的IPv4地址
// 使用inet_addr将点分十进制字符串转换为IPv4地址
ipv4_address = inet_addr(ip_address_str);
// 如果返回的是INADDR_NONE,则表示转换失败
if (ipv4_address == INADDR_NONE) {
perror("Invalid IP address");
exit(1);
}
// 打印IPv4地址的32位整数表示形式(网络字节顺序)
printf("IPv4 Address in binary: %u\n", ipv4_address);
return 0;
}
当我们要将 in_addr 结构体中的IPv4地址转换为字符串表示形式的话,可以使用 inet_ntoa 函数和inet_ntop函数,这两个函数都用于将不同格式的IP地址表示形式进行转换,以便在网络编程中进行处理。inet_ntoa 主要用于IPv4,而 inet_ntop 支持IPv4和IPv6。。但是需要注意的是 inet_ntoa 函数返回一个指向静态内存的指针,因此在多线程环境中不是线程安全的。inet_ntop 是更通用的函数,可以用于IPv4和IPv6地址,而且它是线程安全的,因此在多线程环境中更安全。
inet_ntoa 函数可以将32位二进制形式的IPv4地址转换为点分十进制表示法的字符串。其参数 struct in_addr in 是一个包含32位二进制形式的IPv4地址的 struct in_addr 结构体。该函数返回一个指向表示IPv4地址的字符串的指针。
示例:
#include
#include
#include
#include
int main() {
struct in_addr ipv4_address;
ipv4_address.s_addr = inet_addr("192.168.1.1"); // 32位网络字节序的IPv4地址
if (ipv4_address.s_addr == INADDR_NONE) {
perror("Invalid IP address");
exit(1);
}
// 使用inet_ntoa将32位IPv4地址转换为点分十进制字符串
const char* ip_address_str = inet_ntoa(ipv4_address);
if (ip_address_str == NULL) {
perror("Conversion failed");
exit(1);
}
// 打印转换后的点分十进制字符串
printf("IPv4 Address in dotted decimal notation: %s\n", ip_address_str);
return 0;
}
inet_ntop 函数是将二进制形式的IP地址转换为字符串表示形式。它的参数 int af 为地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。const void *src为指向二进制形式IP地址的内存的指针。char *dst是指向用于存储字符串表示形式IP地址的缓冲区的指针。socklen_t size是缓冲区的大小。该函数如果转换成功,返回指向字符串表示形式IP地址的指针;如果失败,返回 NULL 并设置 errno 以指示错误。
示例:
#include
#include
#include
#include
int main() {
char ip_address_str[INET_ADDRSTRLEN]; // 用于存储转换后的IPv4地址字符串
struct in_addr ipv4_addr;
// 转换IPv4地址
inet_pton(AF_INET, "192.168.1.1", &ipv4_addr);
// 使用inet_ntop将IPv4地址转换为点分十进制字符串
const char* result = inet_ntop(AF_INET, &ipv4_addr, ip_address_str, sizeof(ip_address_str));
if (result == NULL) {
perror("Conversion failed");
exit(1);
}
// 打印转换后的IPv4地址字符串
printf("IPv4 Address in dotted decimal notation: %s\n", ip_address_str);
return 0;
}
文章介绍了Linux网络编程中使用套接字进行编程的流程,以及所需要使用一套接口,对这些接口函数进行了用法介绍,对其中一些结构也进行了分析介绍,最后还介绍了地址转换函数,对地址转换函数的用法以及优缺点都进行了分析。码文不易,如果文章内容对你有帮助的话就点一个👍呗,谢谢你的支持。