liunx网络

1.基础知识

tcp/ip结构
TCP/IP四层OSI七层核心功能
应用层 (消息)应用层/表示层/会话层用户接口与数据处理
传输层(报文端)(数据包)传输层端到端可靠传输
网际层(数据包)网络层路由寻址与逻辑分组
网络接口层 (帧)数据链路层/物理层物理介质访问与帧传输

2.TCP/IP协议数据单元封装

tcp/ip格式

以太网头

目标MAC地址 (6字节)

源MAC地址 (6字节)

类型 (2字节)

载荷(IP数据包,长度可变)

帧校验序列FCS (4字节 CRC)​

​(通常不显示)​

IP头

  • 版本。占4位。指IP的版本,IPv4数据报中该字段的值是4。
  • 首部长度。占4位。以4B为单位,最大可表示的首部长度为60B(15×4B)。最常用的首部长度是20B(5×4B),该字段的值是5,此时不使用任何可选字段。
  • 总长度。占16位,指首部和数据之和的长度,单位为字节,因此数据报的最大长度为65535B。以太网帧的最大传送单元(MTU)为1500B,因此当一个IP数据报封装成帧时,数据报的总长度(首部加数据)一定不能超过下面的数据链路层的MTU值。
  • 标识。占16位。它是一个计数器,每产生一个数据报就加1,并赋值给标识字段。但它并不是“序号”(因为IP是无连接服务)。当一个数据报的长度超过网络MTU时,必须分片,此时每个数据报片都复制一次标识号,以便能正确地重装成原来的数据报。
  • 标志(Flag)。占3位。标志字段的最低位为MF,MF=1表示后面还有分片,MF=0表示最后一个分片。标志字段中间的一位是DF,只有当DF=0时才允许分片。
  • 片偏移。占13位。它指出较长的数据报在分片后,某片在原数据报中的相对位置,片偏移以8B为偏移单位。除最后一个分片外,每个分片的长度一定是8B的整数倍。
  • 生存时间(TTL)。占8位。数据报在网络中可通过的路由器数的最大值,标识数据报在网络中的寿命,以确保数据报不会永远在网络中循环。路由器在转发数据报前,先将TTL减1。若TTL被减为0,则该数据报必须丢弃。
  • 协议。占8位。指出此数据报携带的数据使用何种协议,即数据报的数据部分应上交给哪个协议进行处理,如TCP、UDP等。其中值为6表示TCP,值为17表示UDP。
  • 首部检验和。占16位。它只检验数据报的首部,不包括数据部分,不检验数据部分可减少计算的工作量。数据报每经过一个路由器,其首部中的某些字段(如生存时间、总长度、标志、片偏移、源/目的地址)都可能发生变化,因此路由器都要重新计算首部检验和。首部检验和的计算方法与UDP和TCP的检验和的计算方法相同,
  • 源地址字段。占4B,标识发送方的IP地址。
  • 目的地址字段。占4B,标识接收方的IP地址。

tcp头

  • 源端口和目的端口。各占2B。分别表示发送方和接收方使用的端口号。
  • 序号。占4B,范围为0~2^32-1,共2^32个序号。TCP连接中传送的字节流中的每个字节都要按顺序编号,序号字段值指的是本报文段所发送的数据的第一个字节的序号。例如,一报文段的序号字段值是301,而携带的数据共有100B,表明本报文段的数据的最后一个字节的序号是400,因此下一个报文段的数据序号应从401开始。
  • 确认号。占4B,是期望收到对方下一个报文段的第一个数据字节的序号。若确认号为N,则表明到序号N-1为止的所有数据都已正确收到。例如,B正确收到了A发送过来的一个报文段,其序号字段是501,而数据长度是200B(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
  • 4)数据偏移(首部长度)。占4位,这里不是IP数据报分片的那个数据偏移,而是表示首部长度(首部中还有长度不确定的选项字段),它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。“数据偏移”的单位是32位(以4B为计算单位)。因为4位二教育进制数能表示的最大值为15,所以TCP首部的最大长度为60B。
  • 5)保留。占6位,保留为今后使用,但目前应置为0。
  • 6)紧急位URG。当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)。紧急数据被插入至报文段数据的最前面,而在紧急数据后面的数据仍是普通数据,因此要与首部中的紧急指针字段配合使用。
  • 7)确认位ACK。仅当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
  • 8)推送位PSH(Push)。两个应用进程进行交互式通信时,都希望在键入一个命令后立即就能收到对方的响应,此时发送方TCP把PSH置1,接收方TCP收到PSH=1的报文段后,就尽快交付给接收应用进程,而不再等到整个缓存都填满了后再向上交付。
  • 9)复位位RST(Reset)。当RST=1时,表示TCP连接中出现严重差错(如主机崩溃等),必须释放连接,然后重新建立传输连接。此外,它还可用于拒绝一个非法的报文段。
  • 10)同步位SYN。当SYN=1时表示这是一个连接请求或连接接受报文。当SYN=1,ACK=0时,表明这是一个连接请求报文,若对方同意建立连接,则应在响应报文中使用SYN=1,ACK=1。关于连接的建立和释放,
  • 终止位FIN(Finish)。用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放传输连接。
  • 12)窗口。占2B,范围为0~216-1。窗口值告诉对方,从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。接收方的数据缓存空间是有限的,因此窗口值作为接收方让发送方设置其发送窗口的依据。例如,设确认号是701,窗口字段是1000。这表明,从701号算起,发送此报文段的一方还有接收1000字节数据(字节序号为701~1700)的接收缓存空间。13)检验和。占2B。检验和字段检验的范围包括首部和数据两部分。计算检验和时,和UDP一样,要在TCP报文段的前面加上12B的伪首部(只需将UDP伪首部的协议字段的17改成6,UDP长度字段改成TCP长度,其他的和UDP一样)。
  • 紧急指针。占2B。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据在报文段数据的最前面)。即使窗口为零,也可以发送紧急数据。
  • 选项。长度可变,最长可达40B。当不使用选项时,TCP首部长度是20B。TCP最初只规定了一种选项,即最大报文段长度(Maximum Segment Size,MSS),它是TCP报文段中的数据字段的最大长度。之后又陆续增加了窗口扩大、时间戳等选项,具体请参见教材。
  • 填充。这是为了使整个首部长度是4B的整数倍。

tcp面向连接的可靠的传输

tcp连接(三次握手)

tcp释放

        处于 TIME_WAIT状态的套接字会等待 ​2MSL​(Maximum Segment Lifetime,最大报文段生存时间,通常为 30秒、1分钟或 2分钟)的时间后才会彻底消失。在这段 TIME_WAIT的等待期内,​这个套接字(IP地址和端口的组合)仍然被认为是被占用的

 tcp编程

转换

  • 主机字节序到网络字节序

    • u_long htonl(u_long hostlong);
    • u_short htons(u_short short);
  • 网络字节序到主机字节序
    • u_long ntohl(u_long hostlong);
    • u_short ntohs(u_short short);

ip地址转换

  • int inet_pton(int af, const char* src ,void* dst)
    • IPV4/IPV6 的地址转换成binary格式
    • 使用于IPV4/IPV6
    • 能正确处理255.255.255.255的转换问题
    • 参数:
      • 1.地址协议族(AF_INETAF_INET6
      • 2.src:是一个指针(填写分点形式的IP地址(主要指IPV4))
      • dst:转换的结果给到dst
  • inet_ntop(int af,const void *src,char *dst,socklen_t size)
    • ipv4ipv6的网络字节序变成本地的字符串形式的IP地址
    • 参数
      • 1.af:地址协议族(AF_INETAF_INET6
      • 2.src:是一个指针(32
      • 3.dst:输出结果为32位点分形式的IP地址
      • 4.size:长度

socket函数

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

  • 参数
    • domain
      • AF_INET
      • AF_INET6
      • AF_UNIX,AF_LOCAL
      • AF_NETLINK
      • AF_PACKET
    • type
      • SOCK_STREAM: 流式套接字,唯一对应于TCP
      • SOCK_DGRAM:数据报套接字,唯一对应着UDP
      • SOCK_RAW:原始套接字
    • protocol
      • 一般填0,原始套接字编程时需填充
  • 返回值
    • 成功返回文件描述符
    • 出错返回-1

如果是IPV6编程,要使用struct sockddr_in6结构体(man 7 IPV6,通常使用struct sockaddr_storage来编程。

bind函数

//给一个网络套接字(Socket)分配一个具体的地址和端口号

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

参数
sockfd:通过socket()函数拿到的fd
addr:采用struct sockaddr的结构体地址,通用结构体
struct sockaddr{
sa_family_t sa_family;
char sa_data[4];
}
struct sockaddr_in{ 基于Internel通信结构体
as_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
sin_zero //填充字节,需清零
}
struct in_addr{
uint32_t s_addr;
}
addrlen:地址长度

listen() 函数详解

1. 函数原型与参数

int listen(int sockfd, int backlog);
  • ​**sockfd​:这是通过 socket() 函数创建并已用 bind() 绑定到某个端口的套接字描述符。调用 listen() 后,这个套接字就从 ​主动套接字​(用于发起连接)转变为 ​被动套接字**​(用于接受连接)。
  • ​**backlog​:这是指已完成三次握手(ESTABLISHED)的连接队列的最大长度**。
    • 内核维护两个队列​:
      1. 未完成连接队列(SYN_RCVD 状态)​​:存放已收到客户端SYN包、正在等待完成三次握手的连接。
      2. 已完成连接队列(ESTABLISHED 状态)​​:存放已完成三次握手、等待服务器调用 accept() 取走的连接。

accept()函数

//从内核已完成的连接队列中取出一个连接,并为它创建一个新的套接字

服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    
    // 1. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    
    // 2. 设置套接字选项(避免"Address already in use"错误)
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    
    // 3. 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡
    address.sin_port = htons(PORT);       // 端口转换为网络字节序
    
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    
    // 4. 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    
    printf("Server listening on port %d...\n", PORT);
    
    // 5. 接受客户端连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    
    // 打印客户端信息
    char client_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &(address.sin_addr), client_ip, INET_ADDRSTRLEN);
    printf("Client connected from %s:%d\n", client_ip, ntohs(address.sin_port));
    
    // 6. 通信循环
    while (1) {
        // 读取客户端数据
        ssize_t bytes_read = read(new_socket, buffer, BUFFER_SIZE);
        if (bytes_read < 0) {
            perror("read failed");
            break;
        } else if (bytes_read == 0) {
            printf("Client disconnected\n");
            break;
        }
        
        printf("Received: %s", buffer);
        
        // 回显给客户端
        if (write(new_socket, buffer, bytes_read) < 0) {
            perror("write failed");
            break;
        }
        
        // 清空缓冲区
        memset(buffer, 0, BUFFER_SIZE);
    }
    
    // 7. 关闭连接
    close(new_socket);
    close(server_fd);
    return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define SERVER_IP "127.0.0.1" // 本地回环地址
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    
    // 1. 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    
    // 2. 设置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    
    // 将IP地址从文本转换为二进制形式
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        perror("invalid address/address not supported");
        close(sock);
        exit(EXIT_FAILURE);
    }
    
    // 3. 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connection failed");
        close(sock);
        exit(EXIT_FAILURE);
    }
    
    printf("Connected to server at %s:%d\n", SERVER_IP, PORT);
    
    // 4. 通信循环
    while (1) {
        printf("Enter message (type 'exit' to quit): ");
        fgets(buffer, BUFFER_SIZE, stdin);
        
        // 检查退出命令
        if (strncmp(buffer, "exit", 4) == 0) {
            printf("Exiting...\n");
            break;
        }
        
        // 发送消息给服务器
        if (send(sock, buffer, strlen(buffer), 0) < 0) {
            perror("send failed");
            break;
        }
        
        // 接收服务器回显
        ssize_t bytes_received = read(sock, buffer, BUFFER_SIZE);
        if (bytes_received < 0) {
            perror("read failed");
            break;
        } else if (bytes_received == 0) {
            printf("Server disconnected\n");
            break;
        }
        
        printf("Server echo: %s", buffer);
        
        // 清空缓冲区
        memset(buffer, 0, BUFFER_SIZE);
    }
    
    // 5. 关闭连接
    close(sock);
    return 0;
}

send和write之间的对比

rcv和read之间的对比

都是多了一个flag参数

flag:

        0:和read、write一样

udp

实时性低,用在音视频和dns域名解析

对比send和sendto

因为没有没有建立连接,所以需要再发送的时候添加ip和端口

对比recv和recvfrom

同理

bzero

IO多路复用

1.IO模型

unix/linu下主要有四种I/O模式:
  • 阻塞I/0:·最常用
  • 非阻塞I/0:·可防止进程阻塞在I/0操作上,需要轮询-
  • I/0多路复用:·允许同时对多个I/0进行控制
  • 信号驱动I/0:·一种异步通信模型

2.阻塞I/O

大部分程序使用的都是阻塞模式的I/O
缺省情况下,套接字建立后所处于的模式就是阻塞I/O模式
读操作:read,recv,recvfrom
写操作:write,send      sendto 不阻塞
其他操作:accept,connect
        以read函数为例进程调用read函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数 read将发送阻塞,它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读
经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过read访问这些数据
如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。

        在写操作时发送阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数 据量的情况下。 这时,写操作不进行任何拷贝工作,将发送阻塞。 一但发送缓冲区内有足够多的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。

        UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,
UDP套接字上执行的写操作永远都不会阻塞,sendto

3.非阻塞I/O

当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核"当我请求的I/O操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。"
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停的测试是否一个文件描述符有数据可读(polling)。
应用程序不停的polling内核来检查是否I/O操作已经就绪,这将是一个极浪费CPU资源的操作。 这种模式使用中不普遍。
1.fcntl()函数
        当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。可以使用函数fcntl()
设置一个套接字的标志位为O_NOBLOCK来实现非阻塞。
        int fcntl(int fd, int cmd, long arg)
        int flag;
        flag = fcntl(sockfd,F_GETFL,0)
        flag |= O_NOBLOCK;
        fcntl(sockfd,F_SETFL,flag);
2.ioctl()函数也可以实现
        int b_on = 1;
        ioctl(sock_fd, FIONBIO,&b_on);

4.I/0多路复用

1.TCP多客户端通信要创建多线程或多进程的原因:

  • accept()阻塞导致无法服务新客户:read和accept都是阻塞函数,当read被阻塞时,accept就无法接收新客户
  • accept会生成多个fd

2.基本常识:linux中每个进程最多可以打开1024个文件,最多有1024个文件描述符,文件描述符的特点:

  • 非负整数
  • 从最小可用的数字来分配
  • 每个进程启动时默认打开012三个文件描述符
  • 多路复用针对不止套接字fd,也针对普通的文件描述fd

3.I/0多路复用的基本思想

        把所有的fd放到一个集合里面,监听集合所有的fd是读写事件,如果有就从该fd中读写消息

4.步骤

  • 把相关I/O文件描述符加入到集合(fdset)
  • 调用select函数去监控,等待集合中文件描述数据(阻塞)
  • 当有数据时,select会退出阻塞
  • 依次判断那个文件描述符有数据
  • 依次处理有数据文件描述符的数据

5.函数

1.fd_set
        void FD_zero(fd_set* fdset)              //对集合清零
        void FD_set(int fd, fd_set * fdset)     //把fd加入集合
        void FD_CLR(int fd,fd_set *fdset)    //从集合中清除fd
        void FD_ISSET(int fd,fd_set *fdset) //判断fd是否在fd_set
2.select()
        int select(int maxfdp, //maxfd+1
        fd_set *readfds,         //读集合
        fd_set *writefds,         //写集合     NULL
        fd_set *errorfds,         //异常集合 带外数据才用的到,一般写NULL
        struct timeval *timeout //超时----阻塞多久  
        );
        struct timeval{
                long tv_sec; //秒
                long tv_usec; //微妙
        };
select退出后:集合表示有数据的集合
        if(FD_ISSET(fd,set))
        {
                //1.如果监听套接字有数据,新的客户端进行连接,则accept
                //2.若建立连接的套接字有数据,则去读read
        }

写代码

使用多线程、多进程、IO多路复用等方式多客户端与服务器通信
实现简单qq的通信(两个客户端之间的通信)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值