Linux网络编程

网络编程实际上就是在应用层调用Socket相关的系统调用,Socket是操作系统内核应用层提供的一种进程间通信机制,它使得相同/不同主机上的进程可以都进行通信。两个进程通过Socket连接后,实际上就相当于到了OSI模型的传输层

除了Socket之外,在应用层可能还会使用一些更为高级的网络编程接口,比如httpwebsocket等,这些接口实际上都是对Socket接口的一种更高级别的封装

Linux的Socket库本身并没有客户端、服务器的概念,我们通常是根据某个进程使用Socket的具体行为来把它定为客户端或服务器的。

在进行网络编程时,需要建立连接的每个进程内都需要创建一个Socket对象,且建立连接时会得到一个新的Socket对象。

从类的思想看Socket,其包括IP地址、所使用协议等属性;包括绑定IP地址、进入监听状态,发起连接,建立连接等行为,即是一个封装了网络相关操作的一个类。

1.Linux中Socket相关==系统调用==

1.1创建套接字对象

socket()函数与open()函数类似,如果成功则返回一个文件描述符,该描述符被后续操作所使用

创建Socket对象时,在Linux内核中实际上也会在VFS创建一个struct file文件对象,同时这个实例是sokcet类型的

1
2
3
4
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

1.domain 参数 (协议族/地址族)

该参数指定套接字所使用的协议族或地址族,决定了套接字使用的通信协议。常用的选项包括:

  • AF_INET:IPv4 网络协议。
  • AF_INET6:IPv6 网络协议。
  • AF_UNIX / AF_LOCAL:本地通信(也称作 UNIX 域套接字,适用于同一台机器上的进程间通信)
  • AF_PACKET:用于直接访问底层网络设备。

1.type 参数 (套接字类型)

该参数指定套接字的类型,决定了如何传输数据。常用的选项包括:

  • SOCK_STREAM:面向连接的字节流套接字(通常用于 TCP)
  • SOCK_DGRAM:无连接的数据报套接字(通常用于 UDP)
  • SOCK_RAW:原始套接字,允许程序访问低层协议。
  • SOCK_SEQPACKET:有序且可靠的数据包交付(类似 SOCK_STREAM 但对每个数据块保持边界)

3.protocol 参数 (具体协议)

该参数指定应使用的协议。==通常,这个参数被设为 0,意味着选择默认协议==。如果你明确想要指定协议,则可以选择:

  • IPPROTO_TCP:表示 TCP 协议
  • IPPROTO_UDP:表示 UDP 协议。
  • IPPROTO_ICMP:表示 ICMP 协议
  • IPPROTO_RAW:使用自定义协议,常用于 SOCK_RAW 类型的套接字

在多数情况下,将该参数设置为 0 就会选择默认的协议。例如,如果 domainAF_INETtypeSOCK_STREAM,则 protocol 为 0 会选择 TCP。

1.2绑定IP及端口

通常使用bind()系统调用将创建的一个套接字绑定到指定的IP和端口

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:某个套接字对象的文件描述符

  • bind()函数不是总需要被调用的:一般来讲,会将一个服务器的套接字绑定到一个固定的IP地址,即一个要与服务器通信的客户端事先就知道了服务器的IP地址。如果不绑定IP地址和端口,则程序可以依赖内核的自动选址机制来自动完成地址的选择,通常客户端程序会这样做。

1.3监听

只有服务端需要进入监听状态,等待客户端的连接请求,进入监听状态所使用的函数如下:

1
int listen(int sockfd, int backlog);
  • sockfd:某个套接字对象的文件描述符
  • backlog:请求队列的长度

listen()函数需要在bind()函数之后被调用,在accept()函数之前被调用。

无法在一个已经连接的套接字(即已经成功执行 connect()的套接字或由 accept()调用返回的套接字)上执行 listen()

1.4等待连接请求

服务器在进入监听状态后,等待客户端的连接请求,使用accept()函数可以获取客户端的连接请求并建立连接,其原型如下==(只有服务器才能调用)==:

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 如果调用accept()函数时,服务器并没有收到客户端的连接请求,则服务器进程将进入阻塞态,直到收到客户端的请求为止。

  • 在成功建立连接后,accept()函数将创建一个新的套接字对象并返回一个新的网络文件描述符,这个文件描述符和Socket()返回的不一样,Socket()返回的是服务器(以服务器为例)的套接字的文件描述符,而accept()函数返回套接字连接到调用connet()客户端,服务器通过这个新的套接字与客户端进行交互

1.5请求连接

客户端需要与远程的服务器建立连接以获取资源,其所使用的函数如下:

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 成功调用返回0,否则返回-1

客户端通过 connect()函数请求与服务器建立连接,对于 TCP 连接来说,调用该函数将发生 TCP 连接的握手过程,并最终建立一个TCP连接,而对于UDP协议来说,调用这个函数只是在sockfd中记录服务器IP 地址与端口号,而不发送任何数据。

1.6发送和接收数据

一旦客户端与服务器建立好连接后,就可以使用套接字描述符来收发数据了,客户端需要使用socket()返回的描述符,服务器需要使用accept()返回的描述符

  • 接收数据:可以使用read()函数或者recv()函数,后者可以设置一些标志位来控制如何接受数据
  • 发送数据:可以使用write()函数或者send()函数,后者可以设置一些标志位来控制如何发送数据

在进行I/O操作时,可能会使进程进入阻塞态

1.7关闭套接字

使用close()函数可以关闭套接字,并释放相应资源

1.8使用流程

综上所述,服务端和客户端使用的API其实是有所不同的,看下图总结:

image-20240819153539314

1.9服务端示例代码

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
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
int server_fd;
int client_fd;
char ip_str[20] = {0};
char buf[512] = {0};

server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0)
{
perror("socket error");
exit(EXIT_FAILURE);
}

// 设置并绑定套接字的地址、端口、协议
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听所有地址

int ret = bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret)
{
perror("bind error");
close(server_fd);
exit(EXIT_FAILURE);
}

// 使服务器进入监听状态
ret = listen(server_fd, 50);
if (0 > ret)
{
perror("listen error");
close(server_fd);
exit(EXIT_FAILURE);
}

// 阻塞等待有客户端发起连接
socklen_t addr_len = sizeof(client_addr);
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);

printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端主机的 IP 地址: %s\n", ip_str);
printf("客户端进程的端口号: %d\n", client_addr.sin_port);

// 接收客户端发送过来的数据
while (1)
{
memset(buf, 0x0, sizeof(buf));

// 阻塞读数据
ret = recv(client_fd, buf, sizeof(buf), 0);
if (0 >= ret)
{
perror("recv error");
close(client_fd);
break;
}
printf("from client: %s\n", buf);
}
close(server_fd);
exit(EXIT_SUCCESS);
}

1.10客户端示例代码

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
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
struct sockaddr_in client_addr = {0};
int client_fd;
char ip_str[20] = {0};
char buf[512] = {0};

client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd < 0)
{
perror("socket error");
exit(EXIT_FAILURE);
}

// 设置并绑定套接字的地址、端口、协议
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &client_addr.sin_addr);

// 阻塞连接服务器
int ret = connect(client_fd, (struct sockaddr *)&client_addr, sizeof(client_addr));
if (0 > ret)
{
perror("connect error");
close(client_fd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");

// 发送数据
while (1)
{
memset(buf, 0x0, sizeof(buf));

// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);

// 将用户输入的数据发送给服务器
ret = send(client_fd, buf, strlen(buf), 0);
if (0 > ret)
{
perror("send error");
break;
}
}
close(client_fd);
exit(EXIT_SUCCESS);
}

2.地址转换API

在网络编程时,除了要使用一些系统调用,还有一些C的库函数可以使用,主要用在地址转换方面:

常见的 IP 地址转换 API 包括:

1.inet_pton()

  • 用于将文本形式的 IP 地址(如 "192.168.1.1")转换为二进制格式(如 struct in_addrstruct in6_addr)。
  • pton 代表“presentation to network”(表示形式到网络形式)。

2.inet_ntop()

  • 将二进制格式的 IP 地址(如 struct in_addrstruct in6_addr)转换为文本形式(如 "192.168.1.1")。
  • ntop 代表“network to presentation”(网络形式到表示形式)。

3.inet_addr()inet_aton()

  • 这些函数将文本形式的 IPv4 地址转换为网络字节序的二进制格式。
  • inet_addr() 返回一个 in_addr_t 类型的整数,而 inet_aton() 则将结果存储在 struct in_addr

4.gethostbyname()gethostbyaddr()

  • 这些函数用于将主机名(如 www.example.com)转换为 IP 地址,或将 IP 地址转换为主机名。