上一节简单介绍了一下套接字、字节序和地址结构体的概念,算是对socket
有一个入门的了解。这一节就实现一个客户端-服务端的代码,从这个例子中来学习socket
函数的使用。
文章目录
- 1 客户端/服务端模型
- 2 套接字函数
- 2.1 socket:创建套接字
- 2.2 bind:绑定套接字
- 2.3 listen:监听套接字
- 2.4 accpet:接受客户端连接请求
- 2.5 connect:与服务器套接字建立连接
- 2.6 close:关闭套接字
- 2.7 write和send:写数据
- 2.8 read和recv:读数据
- 3 客户端/服务端模型实现
- 3.1 代码
- 3.2 运行结果
- 3.3 Bind failed:Address already in use
1 客户端/服务端模型
在客户端-服务器模型中,客户端和服务器之间的协作和通信是通过网络实现的,允许用户从远程位置访问和利用服务器上的资源和服务。这种模型的广泛应用使得它成为网络应用程序设计的基本框架之一。对于Linux的socket
来说,建立通信的整体流程如下:
2 套接字函数
首先先来学习一下前面的流程图中出现的一些套接字函数
2.1 socket:创建套接字
我们可以使用socket
函数创建一个套接字,套接字可以是不同类型,例如TCP套接字或UDP套接字。
int socket(int domain, int type, int protocol);
domain
(地址族,Address Family
):常用的有AF_INET
(IPv4地址族)、AF_INET6
(IPv6地址族)和AF_UNIX
(Unix域套接字,用于本地进程之间的通信)type
(套接字类型,Socket Type
),常用的有:SOCK_STREAM
:流式套接字,也称为面向连接的套接字。用于可靠的、面向连接的数据传输,例如TCP。SOCK_DGRAM
:数据报套接字,也称为无连接的套接字。用于无连接、不可靠的数据传输,例如UDP。
protocol
(协议):设置0时系统会自动选择适当的协议,例如,创建TCP套接字会使用IPPROTO_TCP
协议。
2.2 bind:绑定套接字
bind
函数用于将一个套接字绑定到一个特定的IP地址和端口号,以便套接字可以在该地址上接受传入的连接请求或数据包。这是在创建服务器程序时常用的操作,因为服务器通常需要监听特定的地址和端口以等待客户端的连接。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:要绑定的套接字描述符addr
:指向一个struct sockaddr
类型的结构体,其中包含了要绑定的IP地址和端口信息。在上一篇文章中有介绍,这个结构体可以是通用的,也可以是特定的(如IPV4、IPV6特定的)addrlen
:addr
结构体的长度,通常使用sizeof(struct sockaddr)
来获取
2.3 listen:监听套接字
listen
函数用于将服务器套接字(通常是TCP套接字)设置为监听状态,以便它可以接受客户端的连接请求。listen
函数通常与bind
和accept
函数一起使用,以创建一个典型的服务器程序,用于等待客户端连接。
int listen(int sockfd, int backlog);
-
sockfd
:要监听的套接字描述符,通常是一个已经通过bind
绑定到特定地址和端口的套接字 -
backlog
:指定在等待队列中可以排队等待的连接请求的最大数量,即服务器可以同时处理的连接请求的数量
SOMAXCONN
可以作为backlog
的参数,它是一个系统常量,表示系统所支持的最大队列长度。SOMAXCONN
的具体值在内核编译时就已经指定,通常是一个相对较大的正整数。使用 SOMAXCONN
可以确保使用系统支持的最大队列长度,以应对高负载的服务器程序。
如果用户设置的backlog
大于SOMAXCONN
,则backlog
就会设置为SOMAXCONN
。
2.4 accpet:接受客户端连接请求
accept
函数用于在服务器端套接字(通常是被动套接字)上接受客户端的连接请求,创建一个新的套接字,并在新的套接字上与客户端进行通信。
主动套接字和被动套接字?
主动套接字是客户端套接字,使用 connect
主动连接到服务器。被动套接字是服务器套接字,使用 accept
来接受客户端的连接请求。
accept
是套接字编程中非常重要的函数之一,用于建立服务器与客户端之间的通信通道。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
:要接受连接请求的套接字描述符,通常是服务器端套接字,即被动套接字addr
:一个指向struct sockaddr
类型的结构体指针,用于存储客户端的地址信息(IP地址和端口号)。传入NULL
表示不关心客户端地址信息。addrlen
:一个指向socklen_t
类型的指针,用于传入addr
结构体的长度。通常,可以传入NULL
。
2.5 connect:与服务器套接字建立连接
connect
函数用于客户端套接字建立与服务器套接字的连接。它是网络编程中非常重要的函数,用于建立通信链路,以便客户端可以与服务器进行数据传输。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:要连接的客户端套接字的描述符addr
:指向一个struct sockaddr
类型的结构体指针,其中包含了服务器的地址信息,包括 IP 地址和端口号。这个结构体的类型(AF_INET
、AF_INET6
等)应与客户端套接字的类型相匹配。addrlen
:addr
结构体的长度。
2.6 close:关闭套接字
当不再需要使用套接字时,使用close
函数可以释放相关资源,包括文件描述符和系统内核资源。
int close(int sockfd);
sockfd
:要关闭的套接字的文件描述符。
2.7 write和send:写数据
write
函数:通用的文件写入函数,可以用于向文件描述符写入数据,也可以用于套接字。
ssize_t write(int fd, const void *buf, size_t count);
fd
是文件描述符,可以是套接字描述符,也可以是其他文件描述符。buf
是包含要写入数据的缓冲区的指针。count
是要写入的数据字节数。- 返回值是发送成功的字节数,如果发送失败则返回-1,并使用
errno
设置错误代码。
send
函数:专门用于套接字通信的函数,它提供了更多的选项和控制来处理套接字数据发送。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
sockfd
是套接字描述符。 -
buf
是包含要发送数据的缓冲区的指针。 -
len
是要发送的数据字节数。 -
flags
是一组标志,可以用来控制发送操作的行为,例如设置非阻塞发送等。(后续用到再介绍) -
返回值是发送成功的字节数,如果发送失败则返回-1。
2.8 read和recv:读数据
read
函数:通用的文件读取函数,可以用于从文件描述符读取数据,也可以用于套接字。
ssize_t read(int fd, void *buf, size_t count);
fd
是文件描述符,可以是套接字描述符,也可以是其他文件描述符。buf
是接收数据的缓冲区的指针。count
是要读取的数据字节数。- 返回值是接收成功的字节数,如果接收失败则返回-1,并使用全局变量
errno
设置错误代码。
recv
函数:专门用于套接字通信的函数,它提供了更多的选项和控制来处理套接字数据接收。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd
是套接字描述符。buf
是接收数据的缓冲区的指针。len
是要接收的数据字节数。flags
是一组标志,可以用来控制接收操作的行为,例如设置非阻塞接收等。(后续用到再介绍)- 返回值是接收成功的字节数,如果接收失败则返回-1。
3 客户端/服务端模型实现
下面实现一个客户端/服务端模型的例子,具体功能为:客户端发一个字符串给服务端,服务端收到后打印。
3.1 代码
(1)服务端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main() {
// 创建套接字
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("Socket creation failed");
exit(1);
}
// 绑定套接字到IP地址和端口
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);
server_address.sin_addr.s_addr = INADDR_ANY;
//server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
perror("Bind failed");
close(server_socket);
exit(1);
}
// 设置服务器套接字为监听状态
if (listen(server_socket, 5) == -1) {
perror("Listen failed");
close(server_socket);
exit(1);
}
printf("Server listening on port 8080...\n");
// 接受客户端连接
struct sockaddr_in client_address;
socklen_t client_len = sizeof(client_address);
int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_len);
if (client_socket == -1) {
perror("Accept failed");
close(server_socket);
exit(1);
}
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
// 从客户端接收数据
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
perror("Receive failed");
} else {
printf("Client says: %s\n", buffer);
}
// 发送响应给客户端
const char *response = "Hello from server!";
send(client_socket, response, strlen(response), 0);
// 关闭套接字
close(client_socket);
close(server_socket);
return 0;
}
(2)客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main() {
// 创建套接字
int client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket == -1) {
perror("Socket creation failed");
exit(1);
}
// 设置服务器地址和端口
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);
//server_address.sin_addr.s_addr = INADDR_ANY;
server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
// 连接到服务器
if (connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
perror("Connection failed");
close(client_socket);
exit(1);
}
// 发送数据给服务器
const char *message = "Hello from client!";
send(client_socket, message, strlen(message), 0);
// 接收服务器的响应
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
perror("Receive failed");
} else {
printf("Server says: %s\n", buffer);
}
// 关闭套接字
close(client_socket);
return 0;
}
服务端IP地址设置为INADDR_ANY
?
当服务器端将IP地址设置为 INADDR_ANY
时,假设服务器上有多个网卡,那么它将绑定到所有可用的网络接口和IP地址。这意味着服务器将监听所有网络接口上的传入连接请求,而不限制于特定的IP地址。这通常用于创建一个通用的服务器,它可以接受来自任何网络接口和任何IP地址的连接请求。
对于客户端来说,通常不会将IP地址设置为 INADDR_ANY
,因为客户端通常是主动发起连接请求的一方,而不是绑定到特定的IP地址。客户端通常不需要关心绑定到哪个IP地址,而是根据服务器的IP地址和端口号来连接到服务器。
3.2 运行结果
编译后,运行server程序,服务端监听8080端口:
运行client程序后,client与server建立连接,客户端发送的Hello from client!
,服务端收到后打印出来:
3.3 Bind failed:Address already in use
如下图所示,在服务端程序退出后马上再运行服务端,会提示Adress already in use。
当一个套接字绑定到一个特定的IP地址和端口后,如果你关闭该套接字,操作系统通常会保留一段时间,称为TIME_WAIT状态,以确保任何挂起的数据包都能够到达其目的地。这样可以避免在相同的地址和端口上立即重新绑定套接字时出现问题。
解决:
在创建套接字时使用setsockopt
函数设置SO_REUSEADDR
选项,以允许在TIME_WAIT
状态下重新绑定相同的地址和端口。
int reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
在前面的服务端代码中增加上面的代码:
可以看到,在服务端退出后,再马上运行服务端的程序不会出现Address already in use的提示