Linux高并发服务器开发——Linux网络编程

文章目录

网络结构模式

C/S结构

在这里插入图片描述

优缺点

在这里插入图片描述

B/S结构

在这里插入图片描述

优缺点

在这里插入图片描述

MAC地址、IP地址、端口

MAC地址

MAC地址6个字节

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

网卡的物理地址

在这里插入图片描述

IP地址

在这里插入图片描述

IP四个字节

MAC地址具有唯一性,是永远不变的,它就像身份证号,用来唯一确认一台电脑。而IP地址是随电脑所在地域的不同而进行改变的,它像带有邮编的住址信息,IP地址前面的部分就表示了所处的子网。

IP地址编址方式

在这里插入图片描述
在这里插入图片描述

A类IP地址

用于广域网

在这里插入图片描述

A类IP网络地址是00000001——01111110,即1——126

可使用的网络号126个,也就是27-2个【减2的原因由于网络地址全0的IP地址是保留地址意思为"本网络",而网络号为127(即01111111)保留作为本机软件回路测试之用】

B类IP地址

用于城际网络

每个B类地址可连接65534(216 - 2, 因为主机号的各位不能同时为0,1)台主机,Internet有16384(214)个B类地址。

在这里插入图片描述

可以容纳256-2=254台主机

C类IP地址

小规模局域网

在这里插入图片描述

D类IP地址

在这里插入图片描述

特殊的网址

主机号全0为子网网络地址,主机号全1为子网广播地址,这两个地址在任何网络中都不能分配给主机用

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

子网掩码

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

端口

在这里插入图片描述

端口号两个字节

进程号是变化的,所以网络通信是根据唯一的端口号标识

端口类型

在这里插入图片描述

网络模型

OSI 七层参考模型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

TCP/IP 四层模型

在这里插入图片描述
在这里插入图片描述

四层介绍

在这里插入图片描述

数据都要通过物理层进行传输的,物理层通过比特流(可以理解为高低电平分别表示1 0刚好与二进制10对应),数据链路层的数据格式为帧,寻找MAC地址(物理地址),网络层的数据的格式是报文,寻找IP地址,所以通过IP地址就能将网络中的不同主机之间建立连接(会进行路由选择,选择路径嘛),传输层锁定端口(端口标识了进程),就可以找到相应的进程,其实计算机间在网络间通信说白了是不同主机间进程的通信,进程找到了,如何通信就涉及到了通信协议TCP、UDP(数据传输)…然后就到了会话层,传输层找到了要传输的两端,会话层就进行连接,打通这条道,建立通路。表示层就更好理解了,将计算机能识别的东西转换成人能识别的,主要通过解释、压缩、解压缩,加密(保证数据安全)什么的。。应用层就是提供网路服务了。。。。 这一切都是为了给网络层的功能铺路的。

协议

  • 语义是解释控制信息每个部分的意义。它规定了需要发出何种控制信息,以及完成的动作与做出什么样的响应。
  • 语法。语法是用户数据与控制信息的结构与格式,以及数据出现的顺序。
  • 时序。时序是对事件发生顺序的详细说明。(也可称为“同步”)。

语义表示要做什么,语法表示要怎么做,时序表示做的顺序

在这里插入图片描述
在这里插入图片描述

常见协议

在这里插入图片描述
在这里插入图片描述

ssh

在这里插入图片描述

在这里插入图片描述

UDP协议

在这里插入图片描述

TCP协议

在这里插入图片描述
在这里插入图片描述

IP协议

在这里插入图片描述

以太网帧协议

在这里插入图片描述

ARP协议

arp是根据IP找到mac地址

在这里插入图片描述
在这里插入图片描述

cmd终端输入arp -a指令

在这里插入图片描述

ARP协议:通过ip地址查找mac地址

主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。

arp地址解析协议的工作原理:

每台主机在其ARP缓存区中维护一个arp列表,当源主机想发送数据给目的主机的时候,先在自己的arp列表中查找是否有该mac地址,如果有就之间将数据发送给目的主机,如果没有就向本地网段发送一个ARP请求广播包来查询目的之举对应的mac地址。

当本地网络中的所有主机都接受到该arp数据包时,首先检查数据包中的IP是否是自己的IP地址,如果不是,则忽略该数据包。如果是,取出数据包中的源主机的IP和MAC地址,并写入自己的arp列表中,并将自己的mac地址发送给源主机。源主机收到相应包之后,先将目的主机中的IP和mac写入自己的arp列表中,并将数据发送给目的主机。如果没有响应包,则表示ARP查询失败。【广播发送ARP请求,单播发送响应】

arp协议在TCP/IP模型中属于IP层(网络层),在OSI模型中属于链路层

在这里插入图片描述

ff:ff:ff:ff:ff:ff表示给网段中所有机器发送ARP请求

网络通信的过程

封装

在这里插入图片描述

分用

当帧到达目的主机时,将沿着协议栈自底向上依次传递。各层协议依次处理帧中本层负责的头部数据,以获取所需的信息,并最终将处理后的帧交给目标应用程序。这个过程称为分用(demultiplexing)。分用是依靠头部信息中的类型字段实现的。

在这里插入图片描述
在这里插入图片描述

图解网络通信过程

详情可看这篇文章

ip地址查询dns服务器获得,然后通过ip找到子网,子网进行一个广播找到指定目的主机,目的主机返回mac地址,后面就知道mac地址了

在这里插入图片描述

socket介绍

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

字节序

简介

在这里插入图片描述

字节序举例

在这里插入图片描述

检测本机的字节序类型

byteorder.c

/*  
    字节序:字节在内存中存储的顺序。
    小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址
    大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址
*/

// 通过代码检测当前主机的字节序
#include <stdio.h>

union tes {
    short value;    // 2字节
    char bytes[sizeof(short)];     // bytes[2]
}test;

int main() {
    test.value = 0x0102;
    if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
        printf("大端字节序\n");
    } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
        printf("小端字节序\n");
    } else {
        printf("未知\n");
    }


    return 0;
}

在这里插入图片描述

字节序转换函数

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
因为主机为小端,所以htonl()会将数据进行转化。使用ntohl()时默认传入参数为大端,且当前主机为小端,也会发生转化。所以两次结果一致。

bytetrans.c

/*

    网络通信时,需要将主机字节序转换成网络字节序(大端),
    另外一段获取到数据以后根据情况将网络字节序转换成主机字节序。

    // 转换端口
    uint16_t htons(uint16_t hostshort);		// 主机字节序 - 网络字节序
    uint16_t ntohs(uint16_t netshort);		// 网络字节序 - 主机字节序

    // 转IP
    uint32_t htonl(uint32_t hostlong);		// 主机字节序 - 网络字节序
    uint32_t ntohl(uint32_t netlong);		// 网络字节序 - 主机字节序

*/

#include <stdio.h>
#include <arpa/inet.h>

int main() {

    // htons 转换端口
    // 端口两个字节
    unsigned short a = 0x0102;
    printf("a : 0x%x\n", a);
    // 小端转大端
    unsigned short b = htons(a);
    printf("b : 0x%x\n", b);

    printf("========================================\n");

    // htonl 转换IP
    // IP四个字节
    char buf[4] = {192, 168, 1, 100};
    int num = *(int *)buf;
    int sum = htonl(num);
    unsigned char * p = (char *)&sum;

    printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3));

    printf("========================================\n");

    // ntohl
    unsigned char buf1[4] = {1, 1, 168, 192};
    int num1 = *(int *)buf1;
    int sum1 = ntohl(num1);
    unsigned char * p1 = (unsigned char*)&sum1;
    printf("%d %d %d %d\n", *p1, *(p1 + 1), *(p1 + 2), *(p1 + 3));

    // ntohs
    unsigned short a1 = 0x0304;
    printf("a1 : 0x%x\n", a1);
    unsigned short b1 = ntohs(a1);
    printf("b1 : 0x%x\n", b1);
    
    return 0;
}

在这里插入图片描述

关于printf的巧妙用法

test.c

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    unsigned char ip[] = {192, 168, 0, 1};
    int *p = (int*)ip;
    *p = htonl(*p);
    for (int i = 0; i < 4; ++i) 
        printf("%d%c", ip[i], " \n"[i == 3]);
    return 0;
}

在这里插入图片描述
在这里插入图片描述

socket地址

// socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个
socket地址。
// 客户端 -> 服务器(IP, Port)

通用 socket 地址

在这里插入图片描述
在这里插入图片描述

专用 socket 地址

在这里插入图片描述
在这里插入图片描述

IP地址转换函数(字符串IP地址转换成整数 ,主机、网络字节序的转换)

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

在这里插入图片描述

inet_aton函数

man inet_aton

在这里插入图片描述

inet_pton函数

man inet_pton

在这里插入图片描述
在这里插入图片描述

inet_ntop函数

man inet_ntop

在这里插入图片描述

小案例

在这里插入图片描述

iptrans.c

/*
    #include <arpa/inet.h>
    // p:点分十进制的IP字符串,n:表示network,网络字节序的整数
    int inet_pton(int af, const char *src, void *dst);
        af:地址族: AF_INET  AF_INET6
        src:需要转换的点分十进制的IP字符串
        dst:转换后的结果保存在这个里面

    // 将网络字节序的整数,转换成点分十进制的IP地址字符串
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
        af:地址族: AF_INET  AF_INET6
        src: 要转换的ip的整数的地址
        dst: 转换成IP地址字符串保存的地方
        size:第三个参数的大小(数组的大小)
        返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

*/

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    // 创建一个ip字符串,点分十进制的IP地址字符串
    char buf[] = "192.168.1.4";
    unsigned int num = 0;

    // 将点分十进制的IP字符串转换成网络字节序的整数
    inet_pton(AF_INET, buf, &num);
    unsigned char * p = (unsigned char *)&num;
    printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3));

    // 将网络字节序的IP整数转换成点分十进制的IP字符串
    char ip[16] = "";
    const char * str = inet_ntop(AF_INET, &num, ip, sizeof(ip));
    printf("str: %s\n", str);
    printf("ip: %s\n", ip);
    printf("%d\n", ip == str);

    return 0;
}

在这里插入图片描述

TCP通信流程

在这里插入图片描述
在这里插入图片描述

服务器端通信流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

客户端通信流程

在这里插入图片描述

客户端没有绑定的话系统会自动分配一个socket, 而服务端必须绑定,因为send()要求有目的地址作为参数。类似于打电话的时候不需要知道自己的号码,但一定知道对方的号码

socket函数

在这里插入图片描述

协议族

man socket

在这里插入图片描述

协议类型

套接字具有指定的类型,该类型指定通信语义。 目前定义的类型有

在这里插入图片描述

protocol

该协议指定与套接字一起使用的特定协议。 通常,在给定协议族中只存在一个协议来支持特定的套接字类型,在这种情况下,协议可以指定为 0。然而,可能存在许多协议,在这种情况下必须以这种方式指定特定协议。 要使用的协议号特定于要在其中进行通信的“通信域”; 参见协议(5)。 有关如何将协议名称字符串映射到协议编号的信息,请参阅 getprotoent(3)。

在这里插入图片描述

返回值

在这里插入图片描述

bind函数

在这里插入图片描述

man 2 bind

在这里插入图片描述

返回值

在这里插入图片描述

listen函数

在这里插入图片描述

man 2 listen

在这里插入图片描述

Listen() 将 sockfd 引用的套接字标记为被动套接字,即,作为将用于使用accept(2) 接受传入连接请求的套接字。

sockfd 参数是一个文件描述符,引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字。

backlog 参数定义 sockfd 的挂起连接队列可以增长的最大长度。 如果连接请求在队列已满时到达,则客户端可能会收到带有 ECONNREFUSED 指示的错误,或者如果底层协议支持重传,则可能会忽略该请求,以便稍后重新尝试连接成功。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

accept函数

在这里插入图片描述

man 2 accept

在这里插入图片描述
在这里插入图片描述

socklen_t就是unsigned int

在这里插入图片描述

connect函数

在这里插入图片描述

man 2 connect

在这里插入图片描述

write & read函数

在这里插入图片描述

TCP通信实现(服务器端)

在这里插入图片描述
在这里插入图片描述

server.c

// TCP 通信的服务器端

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

int main() {

    // 1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;     // 0.0.0.0
    saddr.sin_port = htons(9999);

    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.监听 
    ret = listen(lfd, 8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
     
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

    // 输出客户端的信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);

    // 5.通信
    char recvBuf[1024] = {0};
    while(1) {
        // 获取客户端的数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1) {
            perror("read");
            exit(-1);
        } else if(num > 0) {
            printf("recv client data: %s\n", recvBuf);
        } else if(num == 0) {
            // 表示客户端断开连接
            printf("client closed...");
            break;
        }

        char *data = "hello, I am server!";
        // 给客户端发送数据
        write(cfd, data, strlen(data));
    }

    // 关闭文件描述符
    close(cfd);
    close(lfd);

    return 0;
}

TCP通信实现(客户端)

在这里插入图片描述

客户端绑定的端口是随机的,客户端里面指定的9999是去连接服务器的这个端口。

client.c

// TCP通信的客户端

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

int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 3.通信
    char recvBuf[1024] = {0};
    while(1) {
        char *data = "hello, I am client!!";
        // 给客户端发送数据
        write(fd, data, strlen(data));

        sleep(1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server data: %s\n", recvBuf);
        } else if(len == 0) {
             // 表示服务器端断开连接
            printf("server closed...\n");
            break;
        }
    }
    // 关闭连接
    close(fd);

    return 0;
}

服务器端客户端通信结果展示

在这里插入图片描述
在这里插入图片描述

回射服务器+自定义内容键盘无限次输入

server.c

// TCP 通信的服务器端

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

int main() {

    // 1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;     // 0.0.0.0
    saddr.sin_port = htons(3333);

    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.监听 
    ret = listen(lfd, 8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
     
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

    // 输出客户端的信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);

    // 5.通信
    char recvBuf[1024] = {0};
    while(1) {
        // 获取客户端的数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1) {
            perror("read");
            exit(-1);
        } else if(num > 0) {
            printf("recv client data: %s\n", recvBuf);
        } else if(num == 0) {
            // 表示客户端断开连接
            printf("client closed...\n");
            break;
        }

        // char *data = "hello, I am server!";
        // 给客户端发送数据
        write(cfd, recvBuf, strlen(recvBuf));
        memset(recvBuf, 0, 1024);
    }

    // 关闭文件描述符
    close(cfd);
    close(lfd);

    return 0;
}

client.c

// TCP通信的客户端

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

int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(3333);
    int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 3.通信
    char recvBuf[1024] = {0};
    char data[1024] = {0};
    while(1) {
        printf("please input data:\n");
        int res = scanf("%[^\n]", data);
        // 消除回车影响
        getchar();
        if(res != -1) {
            // 给客户端发送数据
            write(fd, data, strlen(data));
            sleep(1);

            int len = read(fd, recvBuf, sizeof(recvBuf));
            if(len == -1) {
                perror("read");
                exit(-1);
            } else if(len > 0) {
                printf("recv server data: %s\n", recvBuf);
            } else if(len == 0) {
                // 表示服务器端断开连接
                printf("server closed...\n");
                break;
            }
            
        } else {
            perror("scanf");
            exit(-1);
        }
        memset(recvBuf, 0, 1024);
        memset(data, 0, 1024);
        
    }
    // 关闭连接
    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

TCP三次握手

前两次握手是带不了数据的,最后一次握手其实是可以携带数据的。不过一般我们不去考虑复杂的情况

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结一下:为了确保双方都有发送与接收的能力,第一次客户端给服务端发送请求,服务端知道客户端有发送的能力,于是第二次回信;当客户端收到回信后,知道服务端有接受与发送的能力,且为了让服务器知道自己有接收的能力,于是第三次发送信息,在服务器接收到信息时知道客户端有接收的能力,于是建立好了连接

滑动窗口

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个图里面的ACK应该是ack ,而不是ACK标志位

在这里插入图片描述

mss字段在TCP数据包的首部,是告知对方自己能一次能接收的最大数据长度。
fast sender先告诉slow receiver,我的滑动窗口是4096,一次能接收的最大长度为1460。
然后slow reciever告诉fast sender,我的滑动窗口是6144,一次能接收的最大长度为1024。
所以后面fast sender给slow reciever发送的数据包都是1024字节

在这里插入图片描述
在这里插入图片描述

MSS值(MSS = MUT - IP首部长度 - TCP首部长度)。该MSS值是为了告知对方最大的发送数据大小。

MUT(Maximum Transmission Unit)是指网络通信中的最大传输单元

TCP四次挥手

在这里插入图片描述

在这里插入图片描述

这个图里面的ACK应该是ack ,而不是ACK标志位

在这里插入图片描述

mss字段在TCP数据包的首部,是告知对方自己能一次能接收的最大数据长度。
fast sender先告诉slow receiver,我的滑动窗口是4096,一次能接收的最大长度为1460。
然后slow reciever告诉fast sender,我的滑动窗口是6144,一次能接收的最大长度为1024。
所以后面fast sender给slow reciever发送的数据包都是1024字节

在这里插入图片描述
在这里插入图片描述

知乎的四次挥手内容

在这里插入图片描述

  • 1、A的应用进程先向其TCP发出连接释放报文段(FIN=1,seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。
  • 2、B收到连接释放报文段后即发出确认报文段(ACK=1,ack=u+1,seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。
  • 3、A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。
  • 4、B发送完数据,就会发出连接释放报文段(FIN=1,ACK=1,seq=w,ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。
  • 5、A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL(最大报文段生存时间)后,A才进入CLOSED状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。

第四次挥手为什么要等待2MSL?

  • 保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在2MSL时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到CLOSED状态,若A在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到CLOSED状态。
  • 防止已失效的连接请求报文段出现在本连接中。A在发送完最后一个ACK报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。

为什么是四次挥手?

在这里插入图片描述

多进程实现并发服务器

TCP通信并发

在这里插入图片描述

案例

server_process.c

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

void recycleChild(int arg) {
    while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        } else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0) {
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {
    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recycleChild;

    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);

    // 创建socket
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;     // 0.0.0.0
    saddr.sin_port = htons(9999);

    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听 
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {
        // 接受连接
        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        
        if(cfd == -1) {
            if(errno == EINTR) {
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 每一个连接进来, 创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {
            // 子进程
            // 获取客户端的信息 
            char cliIP[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is %s, port is %d\n", cliIP, cliPort);

            // 接受客户端发来的数据
            char recvBuf[1024] = {0};
            while(1) {
                // 获取客户端的数据
                int len = read(cfd, recvBuf, sizeof(recvBuf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len > 0) {
                    printf("recv client data: %s\n", recvBuf);
                } else if(len == 0) {
                    // 表示客户端断开连接
                    printf("client closed...\n");
                    break;
                }

                // char *data = "hello, I am server!";
                // 给客户端发送数据
                write(cfd, recvBuf, len);
                memset(recvBuf, 0, 1024);
            }
            close(cfd);
            // 退出当前子进程
            exit(0);
        }

    }
    // 关闭文件描述符
    close(lfd);

    return 0;
}

client.c

// TCP通信的客户端

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

int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 3.通信
    char recvBuf[1024] = {0};
    // char data[1024] = {0};
    int i = 0;
    while(1) {
        // printf("please input data:\n");
        sprintf(recvBuf, "data : %d\n", i++);
        // int res = scanf("%[^\n]", data);
        // 消除回车影响
        // getchar();

        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);
        sleep(1);
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }
        memset(recvBuf, 0, 1024);
    }
    // 关闭连接
    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

如果在不通过信号,直接用waitpid的话,如果不加while(1),执行waitpid的时候没有子进程,子进程无法回收,加上waitpid一次只能回收一个子进程,所以要用while(1),轮询的去回收子进程的资源。不通过信号,父进程就没办法执行别的事情了,包括继续监听,创建子进程…
信号就好像一个程序中断服务,处理完回收子进程的事情再恢复现场继续监听…

父进程中直接调用 wait/waitpid 有个问题,就是可能一直没有新客户端连接,父进程会一直阻塞在 accept() 处,无法执行到 wait 处。所以要设置信号捕捉。这样父进程阻塞在 accept() 处时会被打断去回收子进程。

案例修改版(设置sa_flag 为SA_RESTART)

man 7 signal

在这里插入图片描述

也可以设置sa_flag 为SA_RESTART,这样阻塞函数被打断处理信号完成后,会重新accept。

server_process.c

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

void recycleChild(int arg) {
    while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        } else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0) {
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {
    struct sigaction act;
    act.sa_flags = SA_RESTART;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recycleChild;

    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);

    // 创建socket
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;     // 0.0.0.0
    saddr.sin_port = htons(9999);

    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听 
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {
        // 接受连接
        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        
        if(cfd == -1) {
            // if(errno == EINTR) {
            //     continue;
            // }
            perror("accept");
            exit(-1);
        }

        // 每一个连接进来, 创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {
            // 子进程
            // 获取客户端的信息 
            char cliIP[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is %s, port is %d\n", cliIP, cliPort);

            // 接受客户端发来的数据
            char recvBuf[1024] = {0};
            while(1) {
                // 获取客户端的数据
                int len = read(cfd, recvBuf, sizeof(recvBuf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len > 0) {
                    printf("recv client data: %s\n", recvBuf);
                } else if(len == 0) {
                    // 表示客户端断开连接
                    printf("client closed...\n");
                    break;
                }

                // char *data = "hello, I am server!";
                // 给客户端发送数据
                write(cfd, recvBuf, len);
                memset(recvBuf, 0, 1024);
            }
            close(cfd);
            // 退出当前子进程
            exit(0);
        }

    }
    // 关闭文件描述符
    close(lfd);

    return 0;
}

client.c

// TCP通信的客户端

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

int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 3.通信
    char recvBuf[1024] = {0};
    // char data[1024] = {0};
    int i = 0;
    while(1) {
        // printf("please input data:\n");
        sprintf(recvBuf, "data : %d\n", i++);
        // int res = scanf("%[^\n]", data);
        // 消除回车影响
        // getchar();

        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);
        sleep(1);
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }
        memset(recvBuf, 0, 1024);
    }
    // 关闭连接
    close(fd);

    return 0;
}

1.wait放在父进程中,阻塞,无法接收另外一个服务端
2.如果不用信号,使用waitpid的话,如果不加while(1),执行waitpid一次只能回收一个子进程,但是加上while(1),可以轮询的去回收子进程的资源。但是不通过信号,父进程就没法执行别的事情,包括继续监听,创建子进程

errno的作用

在这里插入图片描述

案例完善版

server_process.c

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

void recycleChild(int arg) {
    while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        } else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0) {
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {
    struct sigaction act;
    act.sa_flags = SA_RESTART;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recycleChild;

    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);

    // 创建socket
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;     // 0.0.0.0
    saddr.sin_port = htons(9999);

    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听 
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {
        
        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        
        if(cfd == -1) {
            // if(errno == EINTR) {
            //     continue;
            // }
            perror("accept");
            exit(-1);
        }

        // 每一个连接进来, 创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {
            // 子进程
            // 获取客户端的信息 
            char cliIP[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIP, sizeof(cliIP));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is %s, port is %d\n", cliIP, cliPort);

            // 接受客户端发来的数据
            char recvBuf[1024];
            while(1) {
                // 获取客户端的数据
                int len = read(cfd, recvBuf, sizeof(recvBuf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len > 0) {
                    printf("recv client data: %s\n", recvBuf);
                } else if(len == 0) {
                    // 表示客户端断开连接
                    printf("client closed...\n");
                    break;
                }

                // char *data = "hello, I am server!";
                // 给客户端发送数据
                write(cfd, recvBuf, strlen(recvBuf) + 1);
                // memset(recvBuf, 0, 1024);
            }
            close(cfd);
            // 退出当前子进程
            exit(0);
        }

    }
    // 关闭文件描述符
    close(lfd);

    return 0;
}

client.c

// TCP通信的客户端

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

int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 3.通信
    char recvBuf[1024] = {0};
    // char data[1024] = {0};
    int i = 0;
    while(1) {
        // printf("please input data:\n");
        sprintf(recvBuf, "data : %d\n", i++);
        // int res = scanf("%[^\n]", data);
        // 消除回车影响
        // getchar();

        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);
        
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }
        
        sleep(1);
    }
    // 关闭连接
    close(fd);

    return 0;
}

多线程实现并发服务器

同一个程序内的线程共享文件描述符表

在这里插入图片描述
server_thread.c

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

struct sockInfo {
    int fd;     // 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];

void *working(void *arg) {
    // 子线程和客户端通信   cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIP[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIP, sizeof(cliIP));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is %s, port is %d\n", cliIP, cliPort);

    // 接受客户端发来的数据
    char recvBuf[1024];
    while(1) {
        // 获取客户端的数据
        int len = read(pinfo->fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            pthread_exit(NULL);
        } else if(len > 0) {
            printf("recv client data: %s\n", recvBuf);
        } else if(len == 0) {
            // 表示客户端断开连接
            pinfo->fd = -1;
            printf("client closed...\n");
            break;
        }

        // char *data = "hello, I am server!";
        // 给客户端发送数据
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
        // memset(recvBuf, 0, 1024);
    }
    close(pinfo->fd);


    return NULL;
}

int main() {
    // 创建socket
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;     // 0.0.0.0
    saddr.sin_port = htons(9999);

    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听 
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {
        struct sockaddr_in arg;
        int len = sizeof(arg);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&arg, &len);
        
        if(cfd == -1) {
            // if(errno == EINTR) {
            //     continue;
            // }
            perror("accept");
            exit(-1);
        }

        // 堆内存中开辟数据
        struct sockInfo *pinfo;
        for(int i = 0; i < max; i++) {
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
                sleep(1);
                i = -1;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &arg, len);
        // 创建子线程
        pthread_create(&pinfo->tid, NULL, working, pinfo);
        pthread_detach(pinfo->tid);
    }
    close(lfd);
    return 0;
}

client.c

// TCP通信的客户端

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

int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.0.20.6", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 3.通信
    char recvBuf[1024] = {0};
    // char data[1024] = {0};
    int i = 0;
    while(1) {
        // printf("please input data:\n");
        sprintf(recvBuf, "data : %d\n", i++);
        // int res = scanf("%[^\n]", data);
        // 消除回车影响
        // getchar();

        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);
        
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }
        
        sleep(1);
    }
    // 关闭连接
    close(fd);

    return 0;
}

在这里插入图片描述

pthread_exit(NULL)和exit(-1)的区别

在这里插入图片描述

TCP状态转换(三次握手和四次挥手)

在这里插入图片描述

在这里插入图片描述
time_wait状态是主动关闭的一方才有的

红色是客户端状态改变,绿色是服务端状态改变
在这里插入图片描述

为什么是四次挥手而不是三次挥手

因为是一方主动断开的,而另外一方可能还有数据没有传过去,所以ACK和FIN要分开
在这里插入图片描述

2MSL(Maximum Segment Lifetime)

在这里插入图片描述

结论

灵魂拷问:
第一次挥手丢失,会发生什么?
答:主动断开方重传FIN请求报文
第二次挥手丢失,会发生什么?
答:由于ACK报文不会重传,所以主动断开方会重传FIN报文
第三次挥手丢失,会发生什么?
答:被动断开方会重传FIN报文
第四次挥手丢失,会发生什么?
答:同第三次挥手

半关闭、端口复用

半关闭

在这里插入图片描述

当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。

从程序的角度,可以使用 API 来控制实现半连接状态:

在这里插入图片描述

查看网络相关信息的命令netstat

在这里插入图片描述

SFTP上传文件

在这里插入图片描述

recv函数

man 2 recv

在这里插入图片描述
在这里插入图片描述

send函数

man 2 send

在这里插入图片描述
在这里插入图片描述

只运行服务器,客户端未开启时

在这里插入图片描述

服务器和客户端都开启

在这里插入图片描述

端口复用

在这里插入图片描述

setsockopt函数

在这里插入图片描述

man 2 setsockopt

在这里插入图片描述

端口复用使用的案例代码

tcp_server.c

#include <stdio.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
 
int main(int argc, char *argv[]) {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);
    
    // int optval = 1;
    // setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    int optval = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    // 绑定
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        return -1;
    }

    // 监听
    ret = listen(lfd, 8);
    if(ret == -1) {
        perror("listen");
        return -1;
    }

    // 接收客户端连接
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
        perror("accpet");
        return -1;
    }

    // 获取客户端信息
    char cliIp[16];
    inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(cliaddr.sin_port);

    // 输出客户端的信息
    printf("client's ip is %s, and port is %d\n", cliIp, cliPort );

    // 接收客户端发来的数据
    char recvBuf[1024] = {0};
    while(1) {
        int len = recv(cfd, recvBuf, sizeof(recvBuf), 0);
        if(len == -1) {
            perror("recv");
            return -1;
        } else if(len == 0) {
            printf("客户端已经断开连接...\n");
            break;
        } else if(len > 0) {
            printf("read buf = %s\n", recvBuf);
        }

        // 小写转大写
        for(int i = 0; i < len; ++i) {
            recvBuf[i] = toupper(recvBuf[i]);
        }

        printf("after buf = %s\n", recvBuf);

        // 大写字符串发给客户端
        ret = send(cfd, recvBuf, strlen(recvBuf) + 1, 0);
        if(ret == -1) {
            perror("send");
            return -1;
        }
    }
    
    close(cfd);
    close(lfd);

    return 0;
}

tcp_client.c

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
 
int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    while(1) {
        char sendBuf[1024] = {0};
        // fgets会阻塞
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
    }

    close(fd);

    return 0;
}

先关闭服务端 那么服务端进入到FIN_WAIT_2(这个状态会持续1-2分钟) 此时客户端写数据 不会发生错误 write的返回值大于0 但是在下面read会返回0(因为服务端已经关闭) 所以客户端也close 就关闭了

IO多路复用简介(I/O多路转接)

I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。

在这里插入图片描述
在这里插入图片描述

BIO模型

阻塞等待

在这里插入图片描述
在这里插入图片描述

NIO模型

非阻塞, 忙轮询

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

IO多路转接技术

第一种:select / poll

在这里插入图片描述
在这里插入图片描述

第二种:epoll

在这里插入图片描述
在这里插入图片描述

select API介绍

在这里插入图片描述

状态是 1 还是 0 是每个 fd 都要检测一次的。 只是说 状态为0的fd文件就不用去检测缓冲区。只检测状态为1的对应文件的缓冲区,如果没有数据到达,就把比特位改动为0。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

man 2 select

在这里插入图片描述

select()工作过程分析

在这里插入图片描述

具体分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

select代码编写

fd_set本质是个long int类型的数组

在这里插入图片描述

在这里插入图片描述

select函数IO多路复用小案例实现

在这里插入图片描述

select调用前rdset的某位fd为1表示我们希望内核帮我们检测该fd对应的接收缓存。select调用后rdset对应的fd为1表示该接收缓存接收到数据了,为0表示没接收到数据

select.c

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

int main() {
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    // 绑定所有网卡上的ip
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 创建一个fd_set的集合,存放的是需要检测的文件描述符
    // 给内核修改的是tmp, 用户态用rdset用来进行FD_SET、FD_ZERO等操作
    fd_set rdset, tmp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1) {
        tmp = rdset;

        // 调用select系统函数,让内核帮检测哪些文件描述符有数据
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if(ret == -1) {
            perror("select");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(FD_ISSET(lfd, &tmp)) {
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                FD_SET(cfd, &rdset);

                // 更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd : cfd;
            }

            for(int i = lfd + 1; i <= maxfd; i++) {
                if(FD_ISSET(i, &tmp)) {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                } 
            }
        }
    }
    close(lfd);

    return 0;
}

client.c

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

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        sleep(1);
        // usleep(1000);
    }

    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

select的缺点

在这里插入图片描述

poll API介绍及代码编写

poll函数

在这里插入图片描述

man 2 poll

在这里插入图片描述
在这里插入图片描述

poll多路复用

在这里插入图片描述

poll多路复用小案例

但这个代码有个小问题,比如accept建立连接了,但是因fds数组已满,accept返回的文件描述符无法加入到 fds 中,那么下一次循环时该文件描述符的值(代码中的 cfd )已经被丢弃了。

所以这个地方应该先去看数组中有没有可用的,有的话就accpet连接,没有就下一次在处理

poll.c

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

int main() {
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    // 绑定所有网卡上的ip
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }

    fds[0].fd = lfd;
    int nfds = 0;

    while(1) {
        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1) {
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN) {
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                for(int i = 1; i < 1024; i++) {
                    if(fds[i].fd == -1) {
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        // 更新最大的文件描述符的索引
                        nfds = nfds > i ? nfds : i;
                        break;
                    }
                }
            }

            for(int i = 1; i <= nfds; i++) {
                if(fds[i].revents & POLLIN) {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                } 
            }
        }
    }
    close(lfd);

    return 0;
}

client.c

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

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        sleep(1);
        // usleep(1000);
    }

    close(fd);

    return 0;
}

在这里插入图片描述

epoll API介绍

epoll()多路复用

事件驱动才是检测高效的原因,红黑树节点上注册有回调函数,事件到来后执行回调函数

在这里插入图片描述

epoll_create函数

在这里插入图片描述

man 2 epoll_create

在这里插入图片描述

epoll_ctl函数

在这里插入图片描述

在这里插入图片描述

man 2 epoll_ctl

在这里插入图片描述
在这里插入图片描述

epoll_wait函数

在这里插入图片描述

epoll代码编写

curfd == lfd代表的是服务器监听到有新的客户端连接,因为lfd是服务器负责监听的文件描述符

epoll.c

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

int main() {
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    // 绑定所有网卡上的ip
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }
                // 有数据到达,需要通信
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

client.c

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

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        // usleep(1000);
    }

    close(fd);

    return 0;
}

在这里插入图片描述

epoll的两种工作模式

LT 模式 (水平触发)

在这里插入图片描述

案例代码

epoll_lt.c

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

int main() {
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    // 绑定所有网卡上的ip
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }
                // 有数据到达,需要通信
                char buf[5] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

client.c

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

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        fgets(sendBuf, sizeof(sendBuf), stdin);
        // sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        // usleep(1000);
    }

    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

ET 模式(边沿触发)

文件句柄就是文件描述符

在这里插入图片描述
在这里插入图片描述

案例代码

epoll_et.c

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    // 绑定所有网卡上的ip
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

                // 设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                // 设置边沿触发
                epev.events = EPOLLIN | EPOLLET;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & (EPOLLOUT | !EPOLLIN)) {
                    continue;
                }
                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while((len = read(curfd, buf, sizeof(buf))) > 0) {
                    // 打印数据
                    printf("recv data: %s\n", buf);
                    // write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if(len == 0) {
                    printf("\nclient closed..., fd is : %d\n", curfd);
                } else if (len == -1) {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        // 数据暂时不可用,可以等待一段时间后重试或者进行其他操作
                        // 在非阻塞情况下,数据读完了,我们还继续读,就会产生这个问题
                        printf("data not ready yet\n");
                    } else if (errno == ECONNRESET) {
                        // 连接被对方重置
                        printf("connection reset by peer\n");
                    } else {
                        // 其他错误
                        perror("read");
                        exit(-1);
                    }
                }
            }
        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

client.c

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

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "10.0.20.6", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        fgets(sendBuf, sizeof(sendBuf), stdin);
        // sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);
        memset(sendBuf, 0, sizeof(sendBuf));
        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        // usleep(1000);
    }

    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

UDP通信实现

在这里插入图片描述

sendto函数

在这里插入图片描述

man 2 sendto

在这里插入图片描述

recvfrom函数

在这里插入图片描述

man 2 recvfrom

在这里插入图片描述

UDP通信实现小案例

udp_server.c

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

int main() {
    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);

    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    // 2.绑定
    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
        char recvbuf[128];
        char ipbuf[16];
        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);

        // 接收数据
        recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&cliaddr, &len);

        printf("client IP: %s, Prot: %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)), ntohs(cliaddr.sin_port));

        printf("client send data: %s\n", recvbuf);

        // 发送数据
        sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr*)&cliaddr, len);
    }

    close(fd);
    return 0;
}

udp_client.c

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

int main() {
    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);

    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 服务器的地址信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET, "10.0.20.6", &saddr.sin_addr.s_addr);

    int num = 0;
    
    // 3.通信
    while(1) {
        // 发送数据
        char sendBuf[128];
        sprintf(sendBuf, "hello, I am client%d \n", num++);
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr*)&saddr, sizeof(saddr));

        // 接收数据
        recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
        printf("server send data: %s\n", sendBuf);

        sleep(1);
    }
    close(fd);
    return 0;
}

在这里插入图片描述

sizeof计算的是分配的数组所占的内存空间的大小,不受里面存储的内容影响;strlen计算的是字符串的长度,以’\0’为字符串结束标志。
这里recvfrom要接收一段未知长度的字符串,所以是指定用于接收的缓冲区及缓冲区大小;而sendto要发送一段字符串,并指定发送数据的长度。

广播

向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。

  • a.只能在局域网中使用。
  • b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

广播案例实现

按照接收方需要绑定端口来理解;这个时候服务器是发起方,客户端才是接收方,需要绑定接收端口;

这里服务端是主动给别人发送数据了,不需要手动绑定一个端口了,但是底层肯定还会分配一个端口给他的

bro_server.c

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

int main() {
    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 2.设置广播属性
    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));

    // 3.创建一个广播的地址
    struct sockaddr_in cliaddr;
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(9999);
    inet_pton(AF_INET, "10.0.23.255", &cliaddr.sin_addr.s_addr);

    // 3.通信
    int num = 0;
    while(1) {
        char sendBuf[128];
        sprintf(sendBuf, "hello, client...%d\n", num++);

        // 发送数据
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
        printf("广播的数据: %s\n", sendBuf);
        sleep(1);
    }
    close(fd);
    return 0;
}

bro_client.c

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

int main() {
    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 2.客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
        char buf[128];
        // 接收数据
        int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("server say: %s\n", buf);
    }
    close(fd);
    return 0;
}

在这里插入图片描述
在这里插入图片描述

server只发送数据,要先开启广播设置,指定发送数据目的地client是局域网内的广播地址,并指定广播端口,但它本身不需要bind,因为它自己的端口号是啥(不bind就内核分配)不影响client的端口
client的IP地址必须是局域网的广播地址才行(所以可以使用INADDR_ANY),但端口必须是server广播使用的端口(所以一定要绑定广播端口)

组播(多播)

被动接收的一端都要绑定一个端口,主动发送的一端一般都是系统分配端口

在这里插入图片描述
在这里插入图片描述

组播地址

在这里插入图片描述

设置组播

在这里插入图片描述
在这里插入图片描述

组播小案例

multi_server.c

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

int main() {
    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 2.设置多播的属性,设置外出接口
    struct in_addr imr_multiaddr;
    // 初始化多播地址
    inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
    setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));

    // 3.初始化客户端的地址信息
    struct sockaddr_in cliaddr;
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(9999);
    inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr);

    // 3.通信
    int num = 0;
    while(1) {
        char sendBuf[128];
        sprintf(sendBuf, "hello, client....%d\n", num++);
        // 发送数据
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
        printf("组播的数据:%s\n", sendBuf);
        sleep(1);
    }

    close(fd);
    return 0;
}

multi_client.c

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

int main() {
    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    struct in_addr in;
    // 2.客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    struct ip_mreq opt;
    inet_pton(AF_INET, "239.0.0.10", &opt.imr_multiaddr.s_addr);
    opt.imr_interface.s_addr = INADDR_ANY;

    // 加入到多播组
    setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &opt, sizeof(opt));

    // 3.通信
    while(1) {
        char buf[128];
        // 接收数据
        int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("server say: %s\n", buf);
    }
    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

本地套接字通信

在这里插入图片描述
在这里插入图片描述

本地套接字通信的流程

在这里插入图片描述

服务器端

在这里插入图片描述

man 2 socket

在这里插入图片描述

客户端

在这里插入图片描述
在这里插入图片描述

unlink函数

man 2 unlink

在这里插入图片描述

本地套接字通信小案例

ipc_server.c

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

int main() {
    unlink("server.sock");

    // 1.创建监听的套接字
    int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "server.sock");
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));

    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 100);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 4.等待客户端连接
    struct sockaddr_un cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

    printf("client socket filename: %s\n", cliaddr.sun_path);

    // 5.通信
    while(1) {
        char buf[128];
        int len = recv(cfd, buf, sizeof(buf), 0);
        if(len == -1) {
            perror("recv");
            exit(-1);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        } else if(len > 0) {
            printf("client say : %s\n", buf);
            send(cfd, buf, len, 0);
        }
    }

    close(cfd);
    close(lfd);
    return 0;
}

ipc_client.c

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

int main() {
    unlink("client.sock");

    // 1.创建套接字
    int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(cfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "client.sock");
    int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.连接服务器
    struct sockaddr_un seraddr;
    seraddr.sun_family = AF_LOCAL;
    strcpy(seraddr.sun_path, "server.sock");
    ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 4.通信
    int num = 0;
    while(1) {

        // 发送数据
        char buf[128];
        sprintf(buf, "hello, i am client %d\n", num++);
        ret = send(cfd, buf, strlen(buf) + 1, MSG_NOSIGNAL);
        if(ret == -1) {
            if(errno == EPIPE) {
                printf("server closed....\n");
                break;
            } else {
                perror("send");
                exit(-1);
            } 
        }
        printf("client say : %s\n", buf);

        // 接收数据
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(len == -1) {
            perror("recv");
            exit(-1);
        } else if(len == 0) {
            printf("server closed....\n");
            break;
        } else if(len > 0) {
            printf("server say : %s\n", buf);
        }

        sleep(1);

    }

    close(cfd);
    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

  • 33
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值