基于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发送