socket的创建和连接

Raven005 Lv3

socket编程

socket的概念

Socket(套接字)是计算机网络编程中的一个基本概念,它为应用程序提供了访问底层网络协议的接口,使得进程间能够通过网络进行通信。在更广泛的意义上,Socket 是操作系统提供的一个抽象层,用于简化网络编程并隐藏复杂的网络协议细节。

Socket 是两个进程间通信的端点,每个 Socket 都有唯一的地址,这个地址包括 IP 地址和端口号。IP 地址用于定位网络上的主机,而端口号则用于区分同一台主机上的不同服务。

socket的类型

socket 可以分为几种类型,主要依据它们支持的协议和通信模式:

  • 流式 Socket (SOCK_STREAM): 基于 TCP 协议,提供面向连接的服务,保证数据的顺序传输,且不会丢失数据。
  • 数据报 Socket (SOCK_DGRAM): 基于 UDP 协议,提供无连接服务,数据报文独立传输,不保证数据的顺序和完整性,但传输效率较高。
  • 原始 Socket (SOCK_RAW): 直接访问 IP 层,允许应用程序直接处理 IP 数据包,常用于网络分析或特殊用途的软件。

socket提供的函数

  • socket() 创建一个新的确定类型的套接字,类型用一个整型数值标识,并为它分配系统资源。
  • bind() 一般用于服务器端,将一个套接字与一个套接字地址结构相关联,比如,一个指定的本地端口和IP地址。
  • listen() 用于服务器端,使一个绑定的TCP套接字进入监听状态。
  • connect()用于客户端,为一个套接字分配一个自由的本地端口号。 如果是TCP套接字的话,它会试图获得一个新的TCP连接。
  • accept()用于服务器端。 它接受一个从远端客户端发出的创建一个新的TCP连接的接入请求,创建一个新的套接字,与该连接相应的套接字地址相关联。
  • send()recv(),或者write()read(),或者recvfrom()sendto(),用于往/从远程套接字发送和接受数据。
  • close()用于系统释放分配给一个套接字的资源。 如果是TCP,连接会被中断。
  • gethostbyname()和`gethostbyaddr()
  • select()用于修整有如下情况的套接字列表: 准备读,准备写或者是有错误。
  • poll()用于检查套接字的状态。 套接字可以被测试,看是否可以写入、读取或是有错误。
  • getsockopt()用于查询指定的套接字一个特定的套接字选项的当前值。
  • setsockopt()用于为指定的套接字设定一个特定的套接字选项。

1. socket()

  • 作用:创建一个新的 Socket。
  • 参数
    • int domain:地址族,如 AF_INET(IPv4)或 AF_INET6(IPv6)。
    • int type:Socket 类型,如 SOCK_STREAM(面向连接,流式)或 SOCK_DGRAM(无连接,数据报)。
    • int protocol:协议,通常是 0,表示使用与 type 关联的默认协议。

2. bind()

  • 作用:将 Socket 与本地地址(IP 地址和端口号)相关联。
  • 参数
    • int sockfd:Socket 文件描述符。
    • const struct sockaddr *addr:指向包含地址信息的 sockaddr 结构体的指针。
    • socklen_t addrlensockaddr 结构体的长度。

3. listen()

  • 作用:使 Socket 准备接收连接,通常用于服务器端。
  • 参数
    • int sockfd:Socket 文件描述符。
    • int backlog:待处理连接请求的最大队列长度。

4. accept()

  • 作用:接受传入的连接请求,并返回一个新的 Socket 文件描述符用于通信。
  • 参数
    • int sockfd:监听的 Socket 文件描述符。
    • struct sockaddr *addr:可选参数,用于存储客户端的地址信息。
    • socklen_t *addrlen:可选参数,用于存储地址信息的长度。

5. connect()

  • 作用:初始化与远程主机的连接,通常用于客户端。
  • 参数
    • int sockfd:Socket 文件描述符。
    • const struct sockaddr *addr:指向包含远程主机地址信息的 sockaddr 结构体的指针。
    • socklen_t addrlensockaddr 结构体的长度。

6. send()

  • 作用:发送数据到已连接的 Socket。
  • 参数
    • int sockfd:Socket 文件描述符。
    • const void *buf:指向要发送数据的缓冲区的指针。
    • size_t len:要发送的数据长度。
    • int flags:发送标志,如 MSG_DONTROUTE

7. recv()

  • 作用:从 Socket 接收数据。
  • 参数
    • int sockfd:Socket 文件描述符。
    • void *buf:接收数据的缓冲区。
    • size_t len:缓冲区的大小。
    • int flags:接收标志,如 MSG_PEEK

8. sendto()

  • 作用:向特定地址发送数据,通常用于无连接的 Socket(如 UDP)。
  • 参数:与 send() 类似,额外包括目标地址和地址长度。

9. recvfrom()

  • 作用:接收数据并返回源地址信息,通常用于无连接的 Socket(如 UDP)。
  • 参数:与 recv() 类似,额外包括源地址信息和地址长度。

10. close()

  • 作用:关闭 Socket 文件描述符,释放资源。
  • 参数
    • int sockfd:要关闭的 Socket 文件描述符。

11. setsockopt()

  • 作用:设置 Socket 的选项,如超时、重用地址等。
  • 参数
    • int sockfd:Socket 文件描述符。
    • int level:设置选项的级别,如 SOL_SOCKETIPPROTO_TCP
    • int optname:选项名称。
    • const void *optval:指向选项值的指针。
    • socklen_t optlen:选项值的长度。

12. getsockopt()

  • 作用:获取 Socket 的选项。
  • 参数:与 setsockopt() 类似。

13. shutdown()

  • 作用:关闭 Socket 的读取或写入方向。
  • 参数
    • int sockfd:Socket 文件描述符。
    • int how:关闭的方向,如 SHUT_RD(读取方向)或 SHUT_WR(写入方向)。

注意事项

  • 使用 Socket 编程时,应确保正确处理错误情况,检查每个函数的返回值,并适当地处理任何错误代码。
  • 对于某些函数,如 send()recv(),如果在非阻塞模式下使用,可能需要处理 EAGAIN 或 EWOULDBLOCK 错误码,这表明操作无法立即完成。
  • 在实际编程中,根据具体需求,可能还需要使用到其他函数,例如 select()poll() 用于多路复用,或者 fork()exec() 用于创建子进程处理连接。

TCP socket通信流程

tcp-socket.png

服务器端流程如下:

  1. 创建服务器的socket
  2. 初始化sever_addr(服务器地址)
  3. socketserver_addr绑定 bind
  4. 开始监听listen
  5. 开一个循环保证服务器不会结束,不断的accept接入的客户端请求,进行读写操作writeread (send()和recv()也行)
  6. 关闭socket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SERVER_PORT 10005
#define BUFFLEN 1024
#define BACLOG 10

// 创建新的socket,返回文件描述符
int create_socket() {
int res = socket(AF_INET, SOCK_STREAM, 0);
// NOTE: 如果想要使用UDP协议,只需要将SOCK_STREAM改为SOCK_DGRAM即可
if (res < 0) {
perror("create socket failed");
exit(EXIT_FAILURE);
}
printf("create socket successfully\n");
return res;
}

// 初始化服务器地址信息
void initialize_address(struct sockaddr_in *server_addr) {
memset(server_addr, 0, sizeof(*server_addr));
server_addr->sin_family = AF_INET;
// 监听所有可用地址
server_addr->sin_addr.s_addr = htons(INADDR_ANY);
server_addr->sin_port = htons(SERVER_PORT);
}

// 绑定socket到指定的地址
void try_bind(int socket_fd, const struct sockaddr *server_addr) {
if (bind(socket_fd, server_addr, sizeof(*server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
puts("bind successfully");
}

// 处理客户端请求
void process_client_request(int socket_client) {
char send_buf[BUFFLEN] = {0};
char recv_buf[BUFFLEN] = {0};

// 发送的消息,以方便日后添加或修改
snprintf(send_buf, BUFFLEN, "Hello, this is the server");

while (1) {
ssize_t num_read = read(socket_client, recv_buf, BUFFLEN);
// 客户端断开连接,或读取失败
if (num_read <= 0) {
if (num_read == 0)
puts("Client closed connection");
else
perror("read failed");
return;
}
recv_buf[num_read] = '\0'; // 为接收的字符串添加结束字符

printf("From Client: %s\n", recv_buf);

if (strncmp(recv_buf, "exit", 4) == 0) { // 客户端请求退出
// 修改发送的消息
strcpy(send_buf, "bye!!!");
// 返回消息给客户端
write(socket_client, send_buf, strlen(send_buf));
printf("Me(Server):%s\n", send_buf);
return;
}

// 向客户端写入发送缓冲区的内容
if (write(socket_client, send_buf, strlen(send_buf)) < 0) {
perror("write failed");
return;
}
printf("Me(Server): %s\n", send_buf);

// 清空接收缓冲区,准备下一次读取
memset(recv_buf, 0, BUFFLEN);
}
}

int main() {
int socket_server = create_socket(); // 创建socket
struct sockaddr_in server_addr; // 声明地址结构

initialize_address(&server_addr); // 初始化地址结构
try_bind(socket_server, (struct sockaddr*)&server_addr); // 绑定地址到socket

// 开始监听连接请求,设定最大等待连接数量为BACLOG
if (listen(socket_server, BACLOG) == -1) {
perror("listen failed");
exit(EXIT_FAILURE);
}
puts("start listening");

struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);

while (1) {
// 初始化客户端地址结构
memset(&client_addr, 0, sizeof(client_addr));
// 接受新的连接请求
int socket_client = accept(socket_server, (struct sockaddr*)&client_addr, &client_addr_size);
printf("Client %s:%d connected\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
if (socket_client < 0) {
perror("receive failed");
continue; // 尝试接受另一个连接
}
// 满足要求后开始处理客户端请求
process_client_request(socket_client);
close(socket_client); // 关闭客户端连接
}

// 正常情况下不会执行到这一步
close(socket_server);
return 0;
}


客户端流程如下:

  1. 创建客户端socket
  2. 初始化server_addr
  3. 连接到服务器connect
  4. 利用writeread进行读写操作 (send()和recv()也行)
  5. 关闭socket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SERVER_PORT 10005
#define BUFFLEN 1024

/*
* 创建套接字并返回其文件描述符
* 如果创建失败,程序将以状态1退出
*/
int create_socket() {
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("socket创建失败"); // 使用perror提供更多的错误信息
exit(EXIT_FAILURE);
}
printf("socket create successfully\n");
return sock_fd;
}

/*
* 初始化服务器地址结构的函数
*/
void initialize_address(struct sockaddr_in *server_addr) {
memset(server_addr, 0, sizeof(*server_addr));
server_addr->sin_family = AF_INET;
server_addr->sin_port = htons(SERVER_PORT);
server_addr->sin_addr.s_addr = inet_addr("192.168.5.120"); // 服务器IP
}

/*
* 建立到服务器的连接
* 如果连接失败,程序将以状态1退出
*/
void connect_to_server(int socket_fd, const struct sockaddr *server_addr) {
if (connect(socket_fd, server_addr, sizeof(*server_addr)) < 0) {
perror("connection failed");
exit(EXIT_FAILURE);
}
printf("connection successful\n");
}

/*
* 客户端程序开始的主函数
*/
int main() {
int socket_fd = create_socket(); // 创建socket

struct sockaddr_in server_addr;
initialize_address(&server_addr); // 初始化服务器详细信息

connect_to_server(socket_fd, (struct sockaddr*)&server_addr); // 连接到服务器

char send_buf[BUFFLEN] = {0}; // 用于发送数据的缓冲区
char recv_buf[BUFFLEN] = {0}; // 用于接收数据的缓冲区

// 持续从用户获取输入并发送到服务器,直到从服务器接收到"bye!!!"
while (fgets(send_buf, BUFFLEN, stdin)) {
// 向服务器写入用户输入
if (write(socket_fd, send_buf, strnlen(send_buf, BUFFLEN)) == -1) {
perror("write failed");
break;
}
printf("Me(Client):%s", send_buf); // 打印用户输入
memset(send_buf, 0, BUFFLEN); // 清除发送缓冲区

// 读取服务器的响应
if (read(socket_fd, recv_buf, BUFFLEN) == -1) {
perror("receive failed");
break;
}
printf("Server:%s", recv_buf); // 打印服务器的响应
if (strcmp(recv_buf, "bye!!!") == 0) {
break;
}
memset(recv_buf, 0, BUFFLEN); // 清除接收缓冲区
}

close(socket_fd); // 关闭套接字
return 0;
}