套接字是什么?
服务端接受连接请求的套接字创建过程可分为以下4步:
1、调用socket创建套接字
2、调用bind函数分配IP地址和端口号
3、调用listen函数转换为可接受请求状态
4、调用accept函数受理连接请求
1、调用socket创建套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
使用 socket()
函数创建一个新的套接字,并将其赋值给变量 serv_sock
。
PF_INET
:这个参数指定了套接字的地址族,即协议族。在这里,PF_INET
表示使用 IPv4 地址族。PF_INET
是套接字编程中常用的地址族之一,用于创建基于 IPv4 的套接字。SOCK_STREAM
:这个参数指定了套接字的类型,即套接字的通信方式。在这里,SOCK_STREAM
表示创建一个流式套接字,它是一种可靠的、面向连接的、基于 TCP 协议的套接字。流式套接字提供了可靠的、双向的、面向字节的数据传输,适用于需要可靠数据传输的场景,如文件传输、视频流传输等。0
:这个参数通常被称为协议参数,用于指定要使用的具体协议。在这里,由于我们使用的是SOCK_STREAM
类型的套接字,所以传入0
表示让系统自动选择使用默认的协议,即 TCP 协议。
2、调用bind函数分配IP地址和端口号
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
serv_addr.sin_family = AF_INET;
设置套接字地址结构的地址族为AF_INET
,表示使用 IPv4 地址族。serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
将套接字地址结构中的 IPv4 地址设置为INADDR_ANY
。INADDR_ANY
是一个特殊的 IPv4 地址常量,表示接受来自任意网络接口的连接请求。htonl()
函数用于将主机字节序转换为网络字节序,因为sin_addr.s_addr
是以网络字节序存储的。serv_addr.sin_port = htons(atoi(argv[1]));
将套接字地址结构中的端口号设置为命令行参数argv[1]
中指定的端口号。argv[1]
是一个字符串,需要通过atoi()
函数将其转换为整数形式,然后再使用htons()
函数将端口号转换为网络字节序,因为sin_port
是以网络字节序存储的。
这段代码的作用是将服务器套接字地址结构 serv_addr
初始化为一个特定的值,以便后续在代码中使用该地址结构绑定到服务器套接字上,从而指定服务器监听的地址和端口。
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
这行代码调用了 bind()
函数,将服务器套接字 serv_sock
绑定到指定的地址上。
bind()
函数用于将一个套接字绑定到一个特定的地址和端口上。serv_sock
是要绑定的套接字,即服务器套接字。(struct sockaddr*)&serv_addr
将服务器套接字地址结构serv_addr
强制转换为struct sockaddr*
类型的指针,这是因为bind()
函数接受的地址参数类型为struct sockaddr*
。sizeof(serv_addr)
给出了套接字地址结构的大小,即要绑定的地址结构的字节数。
3、调用listen函数转换为可接受请求状态
listen(serv_sock, 5);
这行代码调用了 listen()
函数,用于设置服务器套接字 serv_sock
开始监听客户端的连接请求。
listen()
函数用于将一个套接字设置为被动模式,开始监听客户端的连接请求。serv_sock
是要监听的服务器套接字。5
是一个参数,表示服务器套接字的连接队列的长度。连接队列用于存放等待被服务器接受的客户端连接请求。如果连接队列已满,新的连接请求将被拒绝。通常情况下,这个参数的值会根据实际需要来设定,通常取值为 5 或 10。
4、调用accept函数受理连接请求
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
这段代码调用了 accept()
函数,用于接受客户端的连接请求,并创建一个新的套接字 clnt_sock
用于与客户端进行通信。
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
- 这行代码调用了
accept()
函数,接受客户端的连接请求,并创建一个新的套接字clnt_sock
用于与客户端进行通信。 serv_sock
是服务器套接字,用于监听客户端的连接请求。(struct sockaddr*)&clnt_addr
是一个指向客户端地址结构的指针,clnt_addr
是用于存储客户端地址信息的变量。accept()
函数将会把客户端的地址信息填充到clnt_addr
中。&clnt_addr_size
是一个指向整数的指针,用于传递客户端地址结构的大小给accept()
函数。在函数调用完成后,clnt_addr_size
将被更新为客户端地址结构的实际大小。
- 这行代码调用了
通过这段代码,服务器将会阻塞等待客户端的连接请求。一旦有客户端连接进来,accept()
函数将会返回一个新的套接字 clnt_sock
,用于与客户端进行通信,并且客户端的地址信息将会填充到 clnt_addr
中。
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
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 msg[] = "Hello world!\n";
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(serv_sock, 5);
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
write(clnt_sock, msg, sizeof(msg));
close(clnt_sock);
close(serv_sock);
return 0;
}
客户端创建套接字
客户端的套接字只有创建和发送连接请求两个步骤
1、调用socket
函数创建套接字
2、调用connect
函数向服务器端发送连接请求
(创建socket前边已经介绍,这里只介绍connect
函数)
2、调用connect
函数向服务器端发送连接请求
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
sock
是客户端套接字,即客户端用于与服务器通信的套接字。
(struct sockaddr*)&serv_addr
是指向服务器地址结构的指针,即服务器的地址信息。
sizeof(serv_addr)
给出了服务器地址结构的大小,即要连接的地址结构的字节数。
通过调用 connect()
函数,客户端套接字 sock
将会尝试连接到指定的服务器地址 serv_addr
上。如果连接成功,客户端将会与服务器建立起连接,可以进行数据交换。如果连接失败,则通常会返回一个错误码,表示连接过程中出现了问题。
需要注意的是,connect()
函数通常会阻塞等待,直到连接成功建立或者连接过程中出现错误。因此,如果需要非阻塞地发起连接,可以使用非阻塞的套接字操作或者使用多线程/多进程技术。
完整程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in serv_addr;
char msg[30];
int str_len;
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
read(sock, msg, sizeof(msg) - 1);
printf("%s", msg);
close(sock);
return 0;
}
打开两个终端,分别编译和运行服务器端程序和客户端程序
# 先编译和运行服务器端
gcc hello_server.c -o hserver
./hserver 10087 # 端口号随便填,但是两边要一致
# 然后编译运行客户端
gcc hello_client.c -o hclient
./hclient 127.0.0.1 10087 # 端口号随便填,但是两边要一致
运行完客户端的程序,就能看到打印 Hello world!
了。