Linux 网络编程笔记

网络基础

网络的诞生是为了实现独立计算机之间的互联,将多台不同且独立的计算机连接在一起。

  • 局域网(LAN):通过交换机和路由器在有限区域内连接多台计算机。

  • 广域网(WAN):连接远距离的计算机,通常是多个局域网的集合。

局域网基础

每层协议都会包含自己的报头,用于标识协议类型。数据包在传输过程中会包含:

  • 报文 = 报头 + 有效载荷

设备若要实现跨网络的数据转发,至少要连接两个网络。

网络协议

协议:一组约定,用于不同厂商的计算机之间顺畅通信。通过网络协议的标准化,各种设备能够遵循统一的标准进行交流。

协议分层

OSI 七层模型

OSI(开放系统互连)模型定义了七层网络协议,用于不同主机类型之间的数据传输:

  1. 应用层

  2. 表示层

  3. 会话层

  4. 传输层

  5. 网络层

  6. 数据链路层

  7. 物理层

TCP/IP 模型

TCP/IP模型是应用广泛的简化模型,适用于实际的网络编程:

  1. 物理层:负责通过物理介质(如光纤、Wi-Fi)传递信号。

  2. 数据链路层:负责数据帧的传送和设备识别。

  3. 网络层:管理地址和路由选择,例如通过路由器建立通信路径。

  4. 传输层:确保两台主机之间的可靠数据传输(如TCP协议)。

  5. 应用层:网络编程主要在应用层进行。

网络传输基本流程

主机 A 的传输流程

  • 应用层(A) 主机 A 的应用层生成数据(文件内容)并打包为应用层数据包,附加应用层协议的头信息。

  • 传输层(A) 应用层数据包传递到传输层(如 TCP 协议),进行分段处理并加上传输层头信息(如端口号、校验和)。每个分段被封装为一个数据段。

  • 网络层(A) 数据段进入网络层,加上网络层头部(如 IP 地址),形成数据包,标明源 IP 和目标 IP(主机 A 和主机 B 的地址)。

  • 数据链路层(A) 网络层数据包进入数据链路层,为数据帧附加数据链路层头部和尾部(如 MAC 地址),包含源 MAC 和目标 MAC 地址信息。此时,数据包被封装为数据帧,准备在物理介质上传输。

网络令牌环机制

  • 在网络令牌环协议中,网络上存在一个称为“令牌”的特殊帧,顺时针或逆时针在各节点间传递。只有持有令牌的节点才有权限发送数据。

  • 主机 A 检测到令牌到达时,抓取令牌并持有令牌进行数据传输。持有令牌后,主机 A 开始向主机 B 发送数据帧。

  • 数据传输完成后,主机 A 将令牌释放,使其重新在网络上流通,以便其他节点获取令牌并发送数据。

物理层

  • 数据链路层的数据帧传输到物理层,转换为电信号或光信号,通过物理介质(如网线、光纤)传输。

其中网络令牌类似

主机 B 的接收流程

  • 物理层(B) 主机 B 的物理层接收信号并将其转换为数据帧。

  • 数据链路层(B) 数据帧传递至数据链路层,进行错误检测,并通过 MAC 地址验证帧的目的地址是否为主机 B。如果符合,数据帧解封装为数据包并传递至上层。

  • 网络层(B) 网络层解封装 IP 头部,确认目的 IP 地址,并将数据包传递给传输层。

  • 传输层(B) 传输层进行重组、校验,确认数据完整性后,将数据传递至应用层。

  • 应用层(B) 最终,数据在应用层解封装,主机 B 的应用程序接收并处理该数据。

数据传输步骤

  • 发送过程:数据在每一层中会被封装上一个报头。

  • 接收过程:数据逐层去除报头(解封装),传递至对应协议。

数据处理的关键点:

  • 如何区分报头和有效载荷。

  • 确保有效载荷传递到每一层的正确协议。


网络中的地址

IP 地址

  • 两种主要协议:IPv4IPv6

  • IPv4:多用于内网和互联网通信,使用4字节(32位)整数表示,以点分十进制格式(如192.168.1.1)。

  • 地址范围:[1.1.1.1 - 254.254.254.254],其中255保留为广播地址。

MAC 地址

  • MAC 地址:在数据链路层用于识别设备的唯一标识符,通常与网卡绑定。

  • 格式:48位,使用十六进制表示,如 00:1A:2B:3C:4D:5E

  • MAC 地址全球唯一,但虚拟机可能会使用虚拟的MAC地址。

TCP 和 UDP

TCP可靠传输协议,提供面向连接的、可靠的、按序传递的数据传输。

UDP不可靠传输协议,是无连接的,不保证数据顺序和完整性。

IP 和 端口号

IP地址:用于唯一标识网络中的一台主机。

端口号:用于唯一标识该主机上服务进程。

IP + 端口:唯一标识一台主机上的服务进程(如IPA + portA标识主机A上的特定进程)。

网络通信的本质

本质上是进程间的通信

进程不一定都提供网络服务或请求,但提供网络服务的进程使用IP + 端口来唯一标识。

客户端到服务器通信中,客户端除了数据还需发送自己的IP和端口给服务器。

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏 移地址也有大端小端之分, 网络数据流同样有大端小端之分.

在网络传输中大端 和小端 传输的数据都默认转化成大端进行网络通讯

大端和小端的定义

  • 大端字节序 (Big-endian):高字节在低地址,低字节在高地址。大端格式类似于人们书写数字的方式(从左到右),例如数值0x1234在内存中表示为12 34(低地址在左,高地址在右)。

  • 小端字节序 (Little-endian):低字节在低地址,高字节在高地址。例如0x1234在小端存储时,低地址保存34,高地址保存12

TCP/IP协议规定,网络数据流要采用大端字节序,即低地址存高字节。

数据先发出的部分存放在低地址,后发出的在高地址。

如果发送主机是小端机器(内存低地址存低字节),就需要将数据转换成大端格式;如果是大端机器,则可直接发送。

OS 网络编程接口

sockaddr 结构体

Socket API中,sockaddr结构提供了一种抽象的网络编程接口,适用于多种底层网络协议(如IPv4、IPv6和UNIX Domain Socket)。虽然每种网络协议的地址格式不同,但Socket API使用struct sockaddr *类型的指针表示通用地址。

sockaddr 的使用

  • 在具体使用时,sockaddr指针通常强制转换为协议特定的地址结构,例如sockaddr_in(用于IPv4)或sockaddr_in6(用于IPv6),以便访问协议对应的地址字段。

  • 这种设计增强了程序的通用性,使同一套代码可以处理不同的网络协议,例如在传参时可以接收IPv4、IPv6或UNIX Domain Socket的sockaddr结构体指针。

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

<sys/socket.h>

参数:

  • domain 指定通讯领域

    AF_INET : IPV4 网络协议域

    AF_INET6: IPV6 网络协议域

    AF_UNIX : UNIX 套接字 (用于本进程间通信)

    AF_LOCAL:AF_UNIX 相同,UNIX 域套接字

    AF_PACKET:与网络设备驱动程序直接通信的原始套接字

  • type 套接字的通信类型

    SOCK_STREAM:提供顺序、可靠、双向连接的基于字节的流(TCP)。

    SOCK_DGRAM:提供无连接的、不可靠的数据报服务(UDP)。

    SCOK_ROW : 原始套接字,访问较低层次的协议

  • protocol 指定了使用的特定协议 (0 系统自动选择合适的协议)

    SOCK_STREAM IPPROTO_TCP(TCP 协议)

    SOCK_DGRAM IPPROTO_UDP(UDP 协议)

返回值 : 成功返回 新的文件描述符 失败返回-1

uint16_t htons(uint16_t hostshort);

#include <arpa/inet.h>

参数:

  • 传递一个 uint16_t hostshort 一个端口号

返回一个用于在网络上传输的端口号 传递给(xxx.sin_port)

in_addr_t inet_addr(const char *cp);

SYNOPSIS #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>

  • 参数

    传递一个 const char * 的IP地址

返回值:

  • 成功则返回一个无符号证书 网络字节序 返回值是大端序的IPv4 地址

  • 如果不是有效的IP 返回 0xffffffff,表示转化失败

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

在网络编程中用于将一个套接字(socket)与一个特定的网络地址(包括IP地址和端口号)关联起来。这个函数允许套接字监听特定的端口,以便能够接收发送到该端口的数据。

#include <sys/socket.h>

参数

  • sockfd

    套接字文件描述符,即通过 socket 函数创建的套接字。

  • addr : 指向一个 sockaddr 结构体的指针

    对于IPv4,这通常是 sockaddr_in 结构体;对于IPv6,则是 sockaddr_in6 结构体

  • addrlen : 指向地址结构体的大小

ssize_t recvfrom()

(int socket, void restrict buffer, size_t length,int flags, struct sockaddr restrict address,socklen_t *restrict address_len);

#include <sys/socket.h>

receive a message from a socket 从网络接收消息

struct sockaddr_in 定义结构体变量 xxx,用于存储发送方的地址信息

参数

  • socket 网络文件描述符fd

  • buffer 缓冲区

  • bufferlength

  • flags 指定接收操作的标志位。通常设置为 0

  • addres: struct sockaddr *restrict sockaddr 结构体的指针,用于存储发送方的地址信息。

  • addreslen : 该变量在调用时应该被初始化为指向的 address 结构体的大小.

返回值:

  • 成功时,返回实际接收到的字节数。

  • 失败时,返回 -1,并设置 errno 以指示错误原因

[[deprecated]] char *inet_ntoa(struct in_addr in);

#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>

将网络字节序列转换成IPv4地址

ssize_t sendto()

#include <sys/socket.h>

ssize_t sendto(int socket, const void message, size_t length, int flags, const struct sockaddr dest_addr, socklen_t dest_len);

sendto — send a message on a socket

参数

  • socket 网络套接字

  • const void *message 指向要发送的缓冲区指针

  • size_t length 指向要发送数据的长度

  • 发送数据的标致

    .....

  • const struct sockaddr *dest_addr 指向目的地址的 sockaddr 结构体的指针

返回值

  • 成功时,返回发送的字节数。

  • 失败时,返回 -1 并设置 errno 以指示错误。

FILE popen(const char command, const char *type);

pipe+fork+exec* = popen

pipe()

int pipe(int filedes[2]);
  • filedes[0] 是读端文件描述符。

  • filedes[1] 是写端文件描述符。

fork()

fork系统调用创建一个新的进程

exec*

exec*系列函数用于在当前进程中执行一个新的程序。当调用exec*函数时,当前进程的地址空间会被新程序的地址空间替换,但进程ID不变。

popen()

FILE *popen(const char *command, const char *type);
  • command 是要执行的命令字符串。

  • type 指定打开模式,可以是"r"(读)或"w"(写)。

它创建一个管道,调用fork,然后在子进程中调用exec*来执行指定的命令。父进程可以通过返回的文件流与子进程通信。

返回值是一个指向FILE流的指针,父进程可以通过这个流读取子进程的输出或将输入发送给子进程。

TCP Server

在udp 写好后 是 可以直接连接的而 Tcp需要建立连接

Tcp是面向字节流的

接口

  • listen(socket,gbacklog);

    socket 设置成监听状态每次监听xxx

  • int accept(int socket, struct sockaddr restrict address,socklen_t restrict address_len);

    • 参数1: socketfd网络文件描述符

    • 参数2:指向 sockaddr 结构的指针 客户端地址信息 输出型参数

    • 参数3:指针指向sockaddr 的长度

accept 函数的返回值是一个文件描述符

accept 参数 和 recvfrom 参数

  • accept 用于服务器端的 TCP 套接字,用于接受客户端的连接请求。

  • recvfrom 用于数据报套接字(如 UDP),用于接收数据并获取发送方的地址信息。

  • accept 返回一个新的文件描述符,用于与客户端通信。

  • recvfrom 返回接收到的字节数。

  • accept 可选地返回连接客户端的地址信息。

  • recvfrom 可选地返回数据发送方的地址信息。

  • accept 用于面向连接的协议

  • recvfrom 用于无连接的协议

recvfrom 直接接收 信息

accept 建立通讯文件描述符后再建立连接

Read 和 Write 函数

Read函数

ssize_t read(int fd, void *buf, size_t count);
  • 从指定的问文件描述符中读取数据

  • fd 文件描述符号

  • buf 指向一个缓冲区

  • count 从文件描述符读取的字节数

成功返回读取的实际字节的数 如果读取的字节数为0 说明已经读取到了字节的末尾EOF

失败返回-1 并设置 errno指示错误类型

Wirte函数

ssize_t write(int fd, const void *buf, size_t count);
  • 向指定的文件描述符写入数据

  • fd 文件描述符

  • buf 指向的缓冲区

  • count 写入的字节数

成功返回写入的字节数

失败返回-1 并设置 errno

connect()函数

int connect(int socket, const struct sockaddr *address,
           socklen_t address_len);

用来建立TCP 连接

  • socket

  • 网络地址

  • 网络地址长度

sudo netstat -altp/-nltp 查看所有的TCP连接

多进程版本/和多线程版本 的TCP通讯的文件描述符问题

守护进程

chen@RE:~/l/m/s/tcpTest|main⚡*
🤓☝️ sleep 100 &                                                                                                      23:53:50
chen@RE:~/l/m/s/tcpTest|main⚡*
🤓☝️ jobs                                                                                                             23:53:54
Job     Group   CPU     State   Command
2       107979  0%      running sleep 100 &
1       107877  0%      running sleep 100 &
chen@RE:~/l/m/s/tcpTest|main⚡*
🤓☝️      

创建多个进程

🤓☝️ ps -axj|head -1 && ps -axj | grep sleep                                                                          23:57:05
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
  23172  108786  108786   23172 pts/5     108864 S     1000   0:00 sleep 100
  23172  108829  108829   23172 pts/5     108864 S     1000   0:00 sleep 100
  23172  108838  108838   23172 pts/5     108864 S     1000   0:00 sleep 100
  23172  108848  108848   23172 pts/5     108864 S     1000   0:00 sleep 100
  23172  108865  108864   23172 pts/5     108864 S+    1000   0:00 grep --color=auto sleep

两个 作业在同一个 会话下

使用fg 命令 将后台的作业提到前台

fg xx

作业是可以相互i前后台转化的

这样的任务会受到用户登录 注销的影响

相应的接口

pid_t setsid(void);

取消自己是组长

🤓☝️cat deamon.hpp                                                                                              15:03:47
#pragma once
​
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <fcntl.h>
​
#define DEV "/dev/null"
​
void dameoSelf(const char * curPath = nullptr)
{
    // 进程忽略异常的信号
​
    signal(SIGPIPE,SIG_IGN);
​
    // 让自己不是改组的组长
​
    if(fork() > 0)exit(0);
​
    // 守护进程是孤儿进程的一种
​
    pid_t n = setsid();
​
    // 守护进程脱离终端   关闭 重定向以前进程默认打开的文件
    // /dev/null  向其中写入的文件全部会丢弃
​
    int fd = open(DEV,O_RDWR);
​
    if(fd > 0)
    {
        dup2(fd,1);
        dup2(fd,0);
        dup2(fd,2);
        close(fd);
    }else{
        close(0);
        close(1);
        close(2);
    }
​
    //   进程执行路径发生更改
​
    if(!curPath)chdir(curPath);
​
}⏎