基于TCP的套接字通信流程

创建套接字

<sys/socket.h>
int socket(int domain, int type, int protocol);
	domain:指定通信所用的协议族,
		AF_INET 表示使用 IPv4 协议族;
        AF_UNIX 或 AF_LOCAL:用于本地通信的协议族;
        AF_INET6:用于 IPv6 协议族的套接字;
        AF_IPX:用于 IPX 协议族的套接字;
        AF_NETLINK:用于内核和用户进程之间的通信;
        AF_PACKET:用于底层数据包操作的协议族。
	type:参数指定套接字的类型
        SOCK_STREAM 表示使用流套接字(TCP)
        SOCK_DGRAM 表示使用数据报套接字(UDP)
        SOCK——RAM 原始套接字,跨传输层的通信--ping
	protocol:参数指定具体的协议,
        通常取值为 0,表示自动选择适合该套接字类型和协议族的默认协议。
返回值:返回一个整数类型的套接字描述符,它作为后续的网络通信操作的参数之一。如果调用失败,socket()函数会返回 -1,并设置全局变量 errno 表示具体的错误原因。

网络相关的结构体

//通用地址结构  
struct sockaddr {  
 sa_family_t sa_family;   // 地址族,如 AF_INET、AF_INET6  
 char        sa_data[14]; // 地址数据  
};  
​  
//ipv4地址结构  
struct in_addr {  
 in_addr_t s_addr; // IP地址,使用网络字节序  
};  
​  
//Internet协议地址结构  
struct sockaddr_in {  
 sa_family_t    sin_family; // 地址族,始终为 AF_INET  
 in_port_t      sin_port;   // 端口号,使用网络字节序(big-endian)  
 struct in_addr sin_addr;   // IP地址,使用网络字节序  
 char           sin_zero[8];// 未使用  
};

例子

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/socket.h>  
#include <arpa/inet.h>  
​  
int main(int argc, char *argv[]) {  
 int sockfd, connfd;  
 struct sockaddr_in server_addr, client_addr;  
 char buffer[1024];  
​  
 // 创建套接字  
 sockfd = socket(AF_INET, SOCK_STREAM, 0);  
 if (sockfd == -1) {  
 printf("Failed to create socket.\n");  
 exit(EXIT_FAILURE);  
 }  
​  
 // 设置服务器地址信息  
 memset(&server_addr, 0, sizeof(server_addr));  
 server_addr.sin_family = AF_INET;  
 server_addr.sin_addr.s_addr = INADDR_ANY;  
 server_addr.sin_port = htons(8888);  
​  
 // 绑定套接字到指定端口  
 if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {  
 printf("Failed to bind socket.\n");  
 exit(EXIT_FAILURE);  
 }  
​  
 // 监听连接请求  
 if (listen(sockfd, 5) != 0) {  
 printf("Failed to listen.\n");  
 exit(EXIT_FAILURE);  
 }  
​  
 // 接受连接请求  
 socklen_t client_len = sizeof(client_addr);  
 connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);  
 if (connfd == -1) {  
 printf("Failed to accept.\n");  
 exit(EXIT_FAILURE);  
 }  
​  
 printf("Connected with client: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));  
​  
 // 接收和发送数据  
 while (1) {  
 memset(buffer, 0, sizeof(buffer));  
 int ret = recv(connfd, buffer, sizeof(buffer), 0);  
 if (ret == -1) {  
 printf("Failed to receive data.\n");  
 exit(EXIT_FAILURE);  
 } else if (ret == 0) {  
 printf("Client disconnected.\n");  
 break;  
 }  
​  
 printf("Received message from client: %s", buffer);  
​  
 ret = send(connfd, buffer, strlen(buffer), 0);  
 if (ret == -1) {  
 printf("Failed to send data.\n");  
 exit(EXIT_FAILURE);  
 }  
 }  
​  
 // 关闭套接字  
 close(connfd);  
 close(sockfd);  
​  
 return 0;  
}

解释

//解释  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/socket.h>  
#include <arpa/inet.h>  
//首先是头文件的引用。这些头文件包含了各种与套接字通信相关的函数和结构体的声明。  
int main(int argc, char *argv[]) {  
 int sockfd, connfd;  
 struct sockaddr_in server_addr, client_addr;  
 char buffer[1024];  
//接着是程序的主函数。这个函数首先定义了一些变量。其中,sockfd 和 connfd 分别是服务器套接字和客户端套接字的文件描述符,server_addr 和 client_addr 分别是服务器地址信息和客户端地址信息的结构体,buffer 是用来存储数据的缓冲区。  
 // 创建套接字  
 sockfd = socket(AF_INET, SOCK_STREAM, 0);  
 if (sockfd == -1) {  
 printf("Failed to create socket.\n");  
 exit(EXIT_FAILURE);  
 }  
//程序接下来创建了服务器套接字。使用 socket 函数创建套接字时,第一个参数指定使用的协议族,一般设置为 AF_INET 表示使用 IPv4 协议;第二个参数指定套接字类型,一般设置为 SOCK_STREAM 表示使用 TCP 协议;第三个参数指定具体的协议,一般设置为 0 表示自动选择协议。  
//如果创建套接字失败,程序会输出错误信息并退出。  
//设置服务器地址信息  
 memset(&server_addr, 0, sizeof(server_addr));  
 server_addr.sin_family = AF_INET;  
 server_addr.sin_addr.s_addr = INADDR_ANY;  
 server_addr.sin_port = htons(8888);  
//接下来是设置服务器地址信息。这里使用了 memset 函数将 server_addr 初始化为 0,然后设置了协议族为 AF_INET、IP 地址为 INADDR_ANY(表示使用本机所有可用的 IP 地址)、端口号为 8888。  
 // 绑定套接字到指定端口  
 if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {  
 printf("Failed to bind socket.\n");  
 exit(EXIT_FAILURE);  
 }  
//接下来是将服务器套接字绑定到指定端口。使用 bind 函数将套接字绑定到指定的端口时,第一个参数是服务器套接字的文件描述符,第二个参数是一个指向 sockaddr 结构体的指针,指定了要绑定的地址信息,第三个参数是这个结构体的大小。如果绑定失败,程序会输出错误信息并退出。  
 // 监听连接请求  
 if (listen(sockfd, 5) != 0) {  
 printf("Failed to listen.\n");  
 exit(EXIT_FAILURE);  
 }  
//接下来是监听连接请求。使用 listen 函数将服务器套接字设为监听状态,第一个参数是服务器套接字的文件描述符,第二个参数是等待连接请求的队列大小。如果监听失败,程序会输出错误信息并退出。  
 printf("Server listening on port %d.\n", ntohs(server_addr.sin_port));  
​  
 while (1) {  
 socklen_t len = sizeof(client_addr);  
​  
 // 接受连接请求,返回客户端套接字  
 connfd = accept(sockfd, (struct sockaddr *)&client_addr, &len);  
 if (connfd < 0) {  
 printf("Failed to accept client connection.\n");  
 continue;  
 }  
​  
 printf("Client connected from %s:%d.\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));  
​  
 // 从客户端接收数据  
 memset(buffer, 0, sizeof(buffer));  
 if (read(connfd, buffer, sizeof(buffer)) < 0) {  
 printf("Failed to receive data from client.\n");  
 } else {  
 printf("Received message from client: %s\n", buffer);  
 }  
​  
 // 向客户端发送数据  
 char *message = "Hello, client!";  
 if (write(connfd, message, strlen(message)) < 0) {  
 printf("Failed to send data to client.\n");  
 }  
​  
 // 关闭客户端套接字  
 close(connfd);  
 }  
​  
 // 关闭服务器套接字  
 close(sockfd);  
 return 0;  
}  
//最后是服务器的主循环。程序使用一个 while 循环不断接受客户端的连接请求,并进行数据的收发。在接受连接请求时,使用 accept 函数返回客户端套接字的文件描述符,如果失败则继续等待下一个连接请求。接着使用 read 函数从客户端套接字中读取数据,如果失败则输出错误信息,否则输出收到的数据。然后使用 write 函数向客户端套接字中写入数据,如果失败则输出错误信息。最后使用 close 函数关闭客户端套接字。如果 while 循环中的代码执行完毕,程序会使用 close 函数关闭服务器套接字并返回 0。  
//总的来说,这个程序使用 TCP 协议创建了一个服务器套接字,监听指定的端口,接受客户端的连接请求,并对连接的客户端进行数据的收发。

分步骤

1. 绑定(bind)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
 sockfd:要绑定的套接字文件描述符。  
 addr:指向要绑定的地址结构体(如 struct sockaddr_in)的指针。  
 addrlen:地址结构体的长度。  
 返回值:函数执行成功后,套接字就与指定的地址和端口号相关联。如果绑定失败,bind() 函数将返回一个负数,表示绑定失败的原因。

在调用 bind() 函数时,第二个参数需要传递一个指向地址结构体的指针,即 const struct sockaddr *addr。但是,server_address 是一个类型为 struct sockaddr_in 的结构体,因此在传递给 bind() 函数之前需要将其转换为 struct sockaddr 类型的指针。

这里采用了一个常用的技巧,即将结构体指针强制转换为通用的 struct sockaddr 指针类型,这样可以避免在函数调用过程中出现类型不匹配的问题。

具体来说,代码中将 server_address 的地址作为参数传递给 bind() 函数时,将其强制转换为 struct sockaddr 类型的指针,即 (struct sockaddr *)&server_address。这样做不会改变地址结构体中的数据内容,只是改变了指针的类型,使得在函数调用中能够通过 struct sockaddr 类型的指针正确地访问 server_address 中的数据。

2. 创建监听队列(listen)
int listen(int sockfd, int backlog);  
 sockfd:已创建并绑定地址的套接字文件描述符;  
 backlog:等待连接队列的最大长度,即同一时刻可以接受的最大连接请求数量。  
 返回值:函数成功返回 0,失败返回 -1。在返回之前,系统会将 sockfd 标识为被动套接字,表示该套接字可以接收客户端连接请求。

需要注意的是,调用 listen() 函数后并不会立即阻塞等待客户端连接请求,需要在调用 accept() 函数前将 sockfd 设置为非阻塞模式,或者在 accept() 函数中使用超时机制等待连接请求的到来。

3. 接受链接(accept)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  
 sockfd:已调用 listen() 函数的套接字文件描述符;  
 addr:用于存放客户端地址信息的结构体指针;  
 addrlen:指向 addr 结构体的长度的指针。  
 返回值:函数成功返回一个新的套接字文件描述符---读写套接字,用于与客户端通信;若失败则返回 -1。
4. 读写数据

可以用read和write去读写

但是UDP中只能用recv和recvfrom接收,send和sendto发送