学习来源

这篇文章主要记录计算机网络的思想,详细的知识细节可以在各种书籍(csapp就很不错)和浏览器中找到,这个博客的目的不在于此

网络层结构

计算机网络的思想是封装的思想,用户在封装的最顶层编写应用程序,网络系统将用户想要传输的数据转换为复杂的可以在各种物理媒介上(一般是网线,无线网络系统中也可以是电磁波)进行传输

了解计算机网络实际上也就是了解这个转换的原理

集线器与局域网(LAN)

在网络系统未引入的背景下,我们想从其他计算机中获取数据就只能通过网线进行传输(涉及IO、中断、缓冲区等内核层面的知识,此处不展开)。 但是如果多台计算机想要共享数据的话就会造成复杂的数据线设计方式,n台计算机就需要n-1根数据线和其他计算机连接,需要消耗2n-1根数据线

所以网络系统设计了一个集线器的东西,将网线的另一端接到集线器上让其做个数据的中间管理员,再把数据发出去,像这样:
集线器

这里的线是电缆线,线上的数据表示最大位宽,这样一个简单的设计就让数据线减少了几乎一倍,但节省最大的还是计算机上连接网络的端口数量

此时发送的数据就有变化了,因为集线器只是不加分辨的将从一个端口上收到的的数据复制给其他端口,此时就会造成数据混乱的问题(其他主机就不知道该数据是否是自己请求的那一份)。

所以在数据的最开始添加两个MAC地址(也叫物理地址)的信息,分别是发送方的mac地址和接收方的mac地址。
此时数据就由两部分组成,标识该数据的的header和数据本身的payload

这样一个主机+电缆+集线器组成的系统就叫以太网(局域网的范围内),范围通常在一个建筑内

网桥(桥)

为了解决集线器不加分辨的将从一个端口上收到的的数据复制给其他端口造成的带宽浪费数据泄露的问题,引入网桥这一物理设备设计了桥接以太网系统: 桥接以太网

网桥将以太网结构进行了再组织,并且可以对数据进一步响应,也就是可以把数据发给指定端口的主机

值得注意的是现代计算机系统中已经将桥的物理设备换成了更高级的集线器(有更多的转发端口,更高的网络带宽等) 网桥与交换机的区别与联系

路由器与互联网

桥接以太网系统任然只是局域网范围内的系统,但想要实现更大范围的主机通信就需要更多的网桥。
而当网络系统覆盖到全世界范围时,所处理的数据就将是千变万化的,并且数据所携带的信息也是各种各样的,此时就要面临兼容性问题。 所以设计出了更高层次的结构互联网络

使用特殊的计算机–路由器 将多个不同的局域网(采用不同的技术实现的局域网,如无线局域网,令牌环网,上面说的以太网等等)系统连接起来。 作为一个计算机,路由器可以执行比集线器和交换机更复杂的功能,比如运行一个协议软件实现一种协议,这种协议消除了不同网络之间的差异。 一般协议需要提供命名机制传送机制两种最基本的功能,这是一个简单的实现示意图: 互联网

这是著名的互联网络系统因特网的实现

因特网

我们可以看到用户的代码种使用了一套名为套接字接口的系统调用,这是网络系统中封装思想的一个具体实现。
使用这套函数(套接字接口本质是一组函数)我们不用关心具体的网络层结构需要的和协议规定的数据格式(也就是header)而集中在数据本身编写程序

数据包

我们写的内容只是蓝色的数据那一部分,而实际发送出去的是这三段

上面是网络系统层级的数据链路层和网络层和传输层的物理实现,完整的大体结构可以参考这个计算机网络的七层结构、五层结构和四层结构

tcp/ip协议

linux套接字接口的设计可以实现任何的底层协议,但其第一个实现的是tcp/ip协议(传输控制协议/互联网络协议),可见其重要性。

该协议实际上是一个协议族,包含了各种各样的不同功能的协议,比如应用层的http协议,传输层的tcp协议udp协议,网络层的ip协议 这里只讲解我见到最多的tcp协议(其他的我也不会)

TCP协议的数据帧的首部共5行,每行32bit一共20bytes

tcp协议

tcp协议也包含了很多协议,比如窗口那一栏的数据是基于滑动窗口协议设计的,长度为16bit。此字段用来进行流量控制。流量控制的单位为字节数,这个值是本端期望一次接收的字节数

以及基与延迟和累计确认方式的确认序号数据段:标识了报文接收端期望接收的字节序列。 如果设置了ACK控制位,确认序号的值表示一个准备接收的包的序列码,它所指向的是准备接收的包,也就是下一个期望接收的包的序列码

需要注意的是tcp/ip协议中的每个层的协议都不是独立的,而是层层递进的,TCP协议就是基于IP协议的基础上传输的。

所以后面的协议中的数据可能但看没有什么具体的意义,但是结合之前的报文来看就有具体的意义了.
比如源端口号表示报文的发送端口,占16位。tcp协议中的源端口和ip协议中的源IP地址组合起来,可以标识报文的发送地址

其他的内容可以自行搜索学习

socket

引入

了解了上面的内容后你会觉得基于网络发送一次数据十分麻烦,所以计算机系统的设计者创造了socket(套接字接口)来封装以上构造数据的操作以及与物理适配器操作的部分

socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。 Socket就是该模式的一个实现,socket即是一种特殊的文件,与其配套的socket函数就是对其进行的操作(读/写IO、打开、关闭)

socket地址结构

从内核角度:一个套接字就是通信的一个端点,(需要补充的是现在进行网络通信的对象已经从上面提到的主机具体到了进程,port就是和pid一样的标识这些进程的数据) 而在linux角度来说套接字就是一个有相应文件描述符的打开文件。套接字接口的基础是套接字地址结构,里面包含了用于通信的基本信息

struct sockaddr_in {
     short            sin_family;     /* 2 字节 ,地址族,e.g. AF_INET, AF_INET6 */
     unsigned short   sin_port;       /* 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490), */
     struct in_addr   sin_addr;       /* 4 字节 ,32位IP地址 */
     char             sin_zero[8];    /* 8 字节 ,不使用 */
};

struct in_addr {
     unsigned long s_addr;            /* 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型*/
}; 

后面的一系列connect,bind,accept,listen函数都是以该地址结构为基础设计的

socket实现客户端-服务器编程模式

在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器(Client/Server, C/S)模式,即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务

服务器端:

其过程是首先服务器方要先启动,并根据请求提供相应服务:

  1. 打开一通信通道并告知本地主机,它愿意在某一公认地址上的某端口(如FTP的端口可能为21)接收客户请求;
  2. 等待客户请求到达该端口;
  3. 接收到客户端的服务请求时,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
  4. 返回第(2)步,等待另一客户请求。
  5. 关闭服务器

客户端:

  1. 打开一通信通道,并连接到服务器所在主机的特定端口;
  2. 向服务器发服务请求报文,等待并接收应答;继续提出请求……
  3. 请求结束后关闭通信通道并终止。

具体实现从上面所描述过程可知:

  1. 客户与服务器进程的作用是非对称的,因此代码不同。
  2. 服务器进程一般是先启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。

基于 TCP 的套接字编程的所有客户端和服务器端都是从调用socket 开始,它返回一个套接字描述符。客户端随后调用connect 函数,服务器端则调用 bind、listen 和accept 函数。套接字通常使用标准的close 函数关闭。 代码实现可以参考:初识tcp–tcp编程部分

简单的nc程序

了解了socket网络编程后就可以简单的实现一个客户端-服务器编程模式程序了。下面的nc程序就是简单的例子,程序有服务端和客户端两种模式 但都只是把收到的数据原封不动的发送回去。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define BUFFER_SIZE 1024

void error(const char *msg) 
{
    perror(msg);
    exit(EXIT_FAILURE);
}

void run_client(const char *address, int port) {
    int sockfd;
    struct sockaddr_in server_addr;  /* 用于socket通信的地址结构体 */
    char buffer[BUFFER_SIZE];        /* 用于io操作的缓冲区 */
    
    /* 创建套接字 */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);/* AF_INET表示我们使用32位ip地址 */
    if (sockfd < 0) {
        error("Socket creation failed");
    }

    /* 用ipv4协议族,地址,端口等信息 初始化socket*/
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    if (inet_pton(AF_INET, address, &server_addr.sin_addr) <= 0)   /* 将ip地址转换位点分十进制 */
    {
        error("Invalid address/ Address not supported");
    }

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        error("Connection failed");
    }

    printf("Connected to %s:%d\n", address, port);
    printf("Type 'exit' to close the connection.\n");

    /*  循环执行io操作 */
    while (1) {
        printf("You: ");
        fgets(buffer, BUFFER_SIZE, stdin);

        /*检查是否接收到 "exit"*/
        if (strncmp(buffer, "exit", 4) == 0) {
            printf("Closing connection...\n");
            break;
        }

        send(sockfd, buffer, strlen(buffer), 0);
	perror("Error TCP send");
        
        int n = recv(sockfd, buffer, BUFFER_SIZE, 0);
        if (n <= 0) {
            printf("Connection closed by server\n");
            break;
        }
        buffer[n] = '\0';
        printf("Server: %s", buffer);
    }

    close(sockfd);
}

void run_server(int port) {
    int sockfd, newsockfd;               /* 单独创建一个监听描述符实现多线程操作 */
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];

    /* 创建套接字 */
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        error("Socket creation failed");
    }

    /* 设置服务器地址 */
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port);

    /* 绑定端口 */
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        error("Bind failed");
    }

    /*  监听连接 */
    if (listen(sockfd, 5) < 0) {
        error("Listen failed");
    }

    printf("Listening on port %d...\n", port);

     /* 接受客户端连接 */
    client_len = sizeof(client_addr);
    newsockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
    if (newsockfd < 0) {
        error("Accept failed");
    }

    printf("Connected to client\n");
    printf("Type 'exit' to close the connection.\n");

     /* 数据传输 */
    while (1) {
        int n = recv(newsockfd, buffer, BUFFER_SIZE, 0);
        if (n <= 0) {
            printf("Connection closed by client\n");
            break;
        }
        buffer[n] = '\0';
        printf("Client: %s", buffer);

        /*检查是否接收到 "exit"*/
        if (strncmp(buffer, "exit", 4) == 0) {
            printf("Closing connection...\n");
            break;
        }

        printf("You: ");
        fgets(buffer, BUFFER_SIZE, stdin);

        /*  检查是否输入了 "exit" */
        if (strncmp(buffer, "exit", 4) == 0) {
            printf("Closing connection...\n");
            send(newsockfd, buffer, strlen(buffer), 0);
            break;
        }

        send(newsockfd, buffer, strlen(buffer), 0);
    }

    close(newsockfd);
    close(sockfd);
}

int main(int argc, char *argv[]) {
    /* 检查命令行参数 */
    if (argc < 4) {
        fprintf(stderr, "Usage: %s -l <port> tcp (for server) or %s <address> <port> tcp (for client)\n", argv[0], argv[0]);
        exit(EXIT_FAILURE);
    }
    
    if (strcmp(argv[1], "-l") == 0)/* 检查程序模式 */
    {
        int port = atoi(argv[2]);
        run_server(port);
    } 
    else 
    {
        const char *address = argv[1];
        int port = atoi(argv[2]);
        run_client(address, port);
    }
return 0;
}
gcc -o nc nc.c

然后命令行输入对应参数即可使用。感兴趣的也可以用gdb调试着玩一下,2025HgamePwnWeek1的ezstack就是考的这个 nc

用python来实现更简单,几十行命令就能搞定,可以自己尝试一下