从零开始:Linux 网络基础到聊天室搭建

2024-09-05 22:35:49 浏览数 (3)

封面封面

浅谈Socket

在拨号上网的时代,上网被看作一个通过与“互联网”这位朋友打电话的行为。这种信息建的交互形成网络,再按照一定规则协议,形成了套接字(Socket)。

早期计算机技术发展过程中,很多术语来自于英文,译者在寻找合适的中文术语时,会结合字面意义和技术特性,偶尔有时会采用音译(ex.鲁棒性robustness)。如果将Socket去翻译软件翻译,得到的会是插座,代表的意思接近一个链接点或接口。根据几个字拆开来再与直译对比来看:

”有包围、套住含义,“”有链接的含义,“”,是一种计算机的单位。

这样组合起来,就表达了这个socket背后的含义,即使表面有点拗口。

现如今,更多接受的翻译方式是不翻译。

到这里可能还是一个模糊的概念,用一个从安装电话到打电话(沟通)的过程来解释这个概念。

一个“套接字”流程一个“套接字”流程
代码语言:c复制
// 该函数用于创建一个套接字
extern int __sys_socket(int family, int type, int protocol);
// 该函数用于将套接字绑定到一个地址上
extern int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen);
// 监听指定套接字的连接请求
extern int __sys_listen(int fd, int backlog);
// 该函数用于接受一个传入的连接请求。
extern int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr, int __user *upeer_addrlen, int flags);

Linux 中的 Socket

如果说在 Java 中,万物皆对象,那么在Linux中可以说万物皆文件。

Socket 也是一种文件,所以 Linux 在网络传输的过程中可以使用文件I/O相关的函数。

代码语言:c复制
// sys_close函数用于关闭一个已打开的文件描述符。
// 参数:  fd - 要关闭的文件描述符。文件,Socket,Pipe都可以传入
// 返回值: 成功时返回0,失败时返回-1,并设置errno。
int sys_close(int fd)

在Linux中创建一个Socket,通过下面的方法实现

代码语言:c复制
// @param family 套接字地址族,如AF_INET表示IPv4
// @param type 套接字类型,如SOCK_STREAM表示TCP流式套接字
// @param protocol 使用的协议,通常为0,系统将自动选择合适的协议
// @return 成功时返回新创建的套接字文件描述符,失败时返回-1并设置errno
int __sys_socket(int family, int type, int protocol);
  • family

这里和font family类似,可以理解为“族”,你可以在linux/include/sockect.h找到支持的全部协议。

代码语言:c复制
/* Supported address families. */
#define AF_UNSPEC	0
#define AF_UNIX		1	/* Unix domain sockets 		*/
#define AF_LOCAL	1	/* POSIX name for AF_UNIX	*/
#define AF_INET		2	/* Internet IP Protocol   常说的IPV4	*/
#define AF_AX25		3	/* Amateur Radio AX.25 		*/
#define AF_IPX		4	/* Novell IPX 			*/
#define AF_APPLETALK	5	/* AppleTalk DDP 		*/
#define AF_NETROM	6	/* Amateur Radio NET/ROM 	*/
#define AF_BRIDGE	7	/* Multiprotocol bridge 	*/
省略...
  • type

类型,相关定义在include/linux/net.h

代码语言:c复制
enum sock_type {
	SOCK_STREAM	= 1,
	SOCK_DGRAM	= 2,
	SOCK_RAW	= 3,
	SOCK_RDM	= 4,
	SOCK_SEQPACKET	= 5,
	SOCK_DCCP	= 6,
	SOCK_PACKET	= 10,
};

第一种流式,比较常见。第二种源码的注释这样描述datagram (conn.less) socket

这二者的关系就像是TCP和UDP。再后面的就是根据不同场景和协议层的不同类型,有的面相传递传递顺序做优化,SOCK_RDM。有的则更多用于自定义,SOCK_RAW。

  • protocol

协议,和地址族支持一致。

Protocol families, same as address families.

TCP建立过程

知道了以上这些基础,则可以创建一个简单的TCP Socket

代码语言:c复制
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0)

但如果想要“打电话”,还需要接一根电话线,知道对方“号码”。

IP 与 Port

当启动一个Spring Boot后,你会熟练的打开127.0.0.1:8080来查看一下是否正常。如果偷懒不想输入端口号,会在application.yml里设置:

代码语言:yml复制
server:
  port: 80

前面的一串数字就是IP,冒号后面的就是端口号。我们创建的socket也需要分配这样一个“组合”。

有很长一段时间里,不明真相的我仍认为IP Port = Socket。

客户端与服务端

客户端(client)与服务端(server),能分得这么清晰的时候,通常是用浏览器访问一个网站,此时浏览器叫做客户端,被访问的网站就是服务端。

还有一个词容易混淆,他就是终端。终端来自于他属于最边缘的设备,客户端通常是终端上的软件。你的手机,电脑就是终端,上面运行的浏览器便是客户端。

如果是两个人在局域网聊天,那双方各自为client和server。所以另一台机器也需要去创建一个Socket且分配IP和Port。

代码语言:c复制
# 绑定到一个IP地址和端口号
server_address = ('0.0.0.0', 8080)  # 监听所有可用的网络接口上的8080端口
socket.bind(server_address)

三次握手

握手握手

三次握手,Three-Way Handshake

及时双方各自为client和server,那在一次请求时,也有发送和接收。发送和接收都有失败的概率,为了收到了你的收到,为了不“黑暗森林”,双方以这样一种形式确定了对接成功。

收发之前,记得先让Socket开始监听

代码语言:c复制
listen(socket, 8888)

下面是一个模拟的过程

代码语言:txt复制
1.客户端发送 SYN 包:
Client -> Server: SYN (ISN = x)

2. 服务器发送 SYN ACK 包:
Server -> Client: SYN (ISN = y), ACK (x   1)

3.客户端发送 ACK 包:
Client -> Server: ACK (y   1)

在这个过程中,每个数据包都包含以下信息:

  • 源 IP 地址和目标 IP 地址
  • 源端口和目标端口
  • 序列号(Sequence Number)
  • 确认号(Acknowledgment Number)
  • 标志位(Flags),如 SYN、ACK 等。

聊天室

服务端

代码语言:c复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    fd_set readfds;

    // 创建Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_id, MAX_CLIENTS) < 0) {
        perror("listen");
        exit(EXITTargetException);
    }

    printf("Chat server started on port 8080n");

    while (1) {
        // 清空文件描述符集合
        FD_ZERO(&readfds);

        // 添加服务器Socket到集合
        FD_SET(server_fd, &readfds);
        int max_sd = server_fd;

        // 添加客户端Socket到集合
        for (int i = 0; i < MAX_CLIENTS; i  ) {
            int sd = client_sockets[i];
            if (sd > 0) {
                FD_SET(sd, &readfds);
            }
            if (sd > max_sd) {
                max_sd = sd;
            }
        }

        // 监听所有Socket
        if (select(max_sd   1, &readfds, NULL, NULL, NULL) < 0) {
            perror("select error");
            exit(EXIT_FAILURE);
        }

        // 处理新的连接
        if (FD_ISSET(server_fd, &readfds)) {
            if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }

            printf("New connection, socket fd is %d, ip is : %s, port : %dn", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));

            // 将新连接添加到客户端Socket数组
            for (int i = 0; i < MAX_CLIENTS; i  ) {
                if (client_sockets[i] == 0) {
                    client_sides[i] = new_socket;
                    break;
                }
            }
        }

        // 处理客户端消息
        for (int i = 0; i < MAX_CLIENTS; i  ) {
            int sd = client_sockets[i];
            if (FD_ISSET(sd, &readfds)) {
                if (read(sd, buffer, BUFFER_SIZE) == 0) {
                    // 客户端断开连接
                    getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                    printf("Host disconnected, ip %s, port %dn", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
                    close(sd);
                    client_sockets[i] = 0;
                } else {
                    // 广播消息给所有客户端
                    buffer[BUFFER_SIZE - 1] = '';
                    printf("Received message: %sn", buffer);
                    for (int j = 0; j < MAX_CLIENTS; j  ) {
                        int client_sd = client_sockets[j];
                        if (client_sd != 0 && client_sd != sd) {
                            send(client_sd, buffer, strlen(buffer), 0);
                        }
                    }
                }
            }
        }
    }

    return 0;
}

客户端

代码语言:c复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_IDEA] = {0};
    char *message = "Hello from client";

    // 创建Socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("n Socket creation error n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 将IP地址转换为二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("nInvalid address/ Address not supported n");
        return -1;
    }

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(servai_addr)) < 0) {
        printf("nConnection Failed n");
        return -1;
    }

    // 发送消息到服务器
    send(sock, message, strlen(message), 0);

    // 接收服务器消息
    int valread = read(sock, buffer, BUFFER_SIZE);
    printf("nServer: %sn", buffer);

    return 0;
}

one more thing

如何优化性能?

从面试最烦人的三次握手开始优化。倘若三次握手还是没成功,会不断尝试,但时间会依次递增,所以可以设置一个三次重试后直接失败返回。

自定义协议。比如使用跳过TCP层的SOCK_RAW类型。(注意风险)

0 人点赞