5 网络编程基础

基本概念

引入

  • 无名管道,有名管道,信号,消息队列,共享内存,信号量只能实现同一主机多个进程间通信
  • 套接字通信实现跨主机通信

网络发展

  1. 第一阶段:APRAnet(阿帕网),冷战的产物,不能实现互联不同类型的计算机和不同类型的操作系统,没有纠错功能
  2. 第二阶段:TCP\IP协议族的体系结构

    协议(protocol):在计算机网络中,要做到有条不紊的交换数据,需要遵循一些约定好的规则,这些规则明确规定了所交换数据的格式以及相关的同步问题;为了进行网络中数据交换而建立的规则、标准和约定统称为网络协议

    • TCP:传输控制协议,用来检测网络传输中的差错控制
    • IP:网际协议,专门负责不同网络间进行互联
  3. OSI(开放系统互联模型)的七层网络体系结构,理想化模型

网络体系结构

  • 每一层有独立的分工,单个每一层不可获取
  • 多个功能相近的协议组织放在同一层,称为协议栈
  • 分层的好处:
    • 各层之间相互独立,每一层不需要知道下一层如何实现,只需要知道该层通过层间接口提供的服务
    • 稳定,灵活,当一层出现变化,只要层间接口保持不变,相邻层不会受影响
    • 易于实现和维护,只需要知道某一层的功能,对指定层维护
    • 促进标准化工作,每一层所提供的服务和技术都有精确的说明
    • 结构上不可分割,各层采用最合适的技术实现

![[三类体系结构.jpg]]

  1. OSI(开放系统互联模型):是由ISO(国际标准化组织)提出的理想化模型,一共有七层
    ![[ISO.jpg]]
  2. TCP\IP协议族的体系结构:互联网实际上的工业标准,一共有四层
    ![[TCP,IP.jpg]]
    虽然TCP\IP体系结构只有四层,但是做的事和OSI的七层体系结构是一样的
  3. TCP\IP四层网络体系结构和OSI七层网络体系结构的对应关系:
    ![[对应关系.jpg]]

TCP\IP协议族中常见协议

每层使用的协议是由下层决定,不能乱用:
![[协议.jpg]]

应用层

  • HTTP(Hypertext Transfer Protocol) 超文本传输协议:万维网的数据通信的基础
  • FTP(File Transfer Protocol) 文件传输协议:是用于在网络上进行文件传输的一套标准协议,使用TCP传输
  • TFTP(Trivial File Transfer Protocol) 简单文件传输协议:是用于在网络上进行文件传输的一套标准协议,使用UDP传输
  • SMTP(Simple Mail Transfer Protocol) 简单邮件传输协议:一种提供可靠且有效的电子邮件传输的协议

传输层

  • TCP(Transport Control Protocol) 传输控制协议:是一种面向连接的、可靠的、基于字节流的传输层通信协议
  • UDP(User Datagram Protocol) 用户数据报协议:是一种无连接、不可靠、快速传输的传输层通信协议

网络层

  • IP(Internetworking Protocol) 网际互连协议:是指能够在多个不同网络间实现信息传输的协议
  • ICMP(Internet Control Message Protocol) 互联网控制信息协议:用于在IP主机、路由器之间传递控制消息、ping命令使用的协议
  • IGMP(Internet Group Management Protocol) 互联网组管理协议:是一个组播协议,用于主机和组播路由器之间通信

链路层

  • ARP(Address Resolution Protocol) 地址解析协议:通过IP地址获取对方mac地址
  • RARP(Reverse Address Resolution Protocol) 逆向地址解析协议:通过mac地址获取ip地址

封包和拆包

  • 网络通信中对数据的封包和拆包过程:
    ![[封包和拆包.jpg]]
  • 对等层(不同主机中的同一层)中数据包名称:
    ![[数据包.jpg]]
  • 一帧数据:
    ![[一帧数据.jpg]]
    • 大小为:64-1518 字节(包含以太网首部14字节,以太网尾部4字节,最大传输单元)
    • 若数据大小大于MTU(最大传输单元,Linux中默认1500),需要分成多个数据包传输
    • ifconfig查看MTU的值

TCP和UDP区别

TCP和UDP都属于传输层的协议

TCP

稳定

  • TCP提供了面向连接的,可靠的传输服务
  • 传输过程中能做到数据无误,数据无丢失,数据无重复,数据无失序
    • TCP通信会给每个数据包编上编号,即序列号
    • 每个序列号都需要应答包应答,若没有应答,数据包会重复传输
  • TCP传输效率较低,消耗资源较多
  • 数据收发不同步:为了提高传输效率,TCP通信中会将多个较小的,发送时间间隔较短的数据包,沾成一个数据包进行发送,即沾包现象
  • 应用场景:对于传输质量要求较高,传输量较大,需要可靠传输信息,例如:用户账号密码登录,大型文件下载

![[TCP.jpg]]

UDP

快速

  • 提供面向无连接,不保证数据可靠传输,尽最大努力传输的协议
  • 数据传输过程中可能会出现数据丢失,重复,失序等现象
  • 传输效率较高,时效性较高
  • 限制每次传输的数据大小,多出部分会直接被忽略删除
  • 数据的收发是同步的,不会沾包
  • 使用场景:发送小尺寸的,在收到数据后给出应答比较困难,例如:广播、组播、通信软件的音视频传输

字节序

字节序:在计算机存储多字节整数时,由于CPU架构,将其分为大端存储和小端存储的主机

  • 大端存储:内存地址低位存储数据的高位
  • 小端存储:内存地址低位存储数据的低位

![[大端存储和小端存储.jpg]]

  • 由于大小端存储问题,可能导致多字节整数传输出现错误,因此,规定网络字节序都是大端存储
  • 传输多字节整数时,发送端先转化为网络字节序,经网络传输到接收端后,再转化为接收端的字节序

验证大端存储/小端存储

#include <myHead.h>

int main(int argc, char const* argv[]) {
    int num = 0x12345678;
    char* p = (char*)(&num);
    //printf("%p : %x\n", p, *p);
    if (*p == 0x12) {
        fputs("大端存储\n", stdout);
    } else if (*p == 0x78) {
        fputs("小端存储\n", stdout);
    }
    return 0;
}
#include <myHead.h>

union Myunion {
    int num;
    char ch;
};

int main(int argc, char const* argv[]) {
    Myunion uni;
    uni.num = 0x12345678;
    if (uni.ch == 0x12) {
        fputs("大端存储\n", stdout);
    } else if (uni.ch == 0x78) {
        fputs("小端存储\n", stdout);
    }
    return 0;
}

转换函数

头文件<arpa/inet.h>

  • uint32_t htonl(uint32_t hostlong);:主机转网络,按无符号四字节转
  • uint16_t htons(uint16_t hostshort);:主机转网络,按无符号两字节转
  • uint32_t ntohl(uint32_t netlong):网络转主机,按无符号四字节转
  • uint16_t ntohs(uint16_t netshort);:网络转主机,按无符号两字节转
#include <myHead.h>

int main(int argc, char const* argv[]) {
    uint32_t num = 0x12345678;
    uint32_t result = htonl(num);
    printf("%x\n", result);    //0x78563412
    return 0;
}
  1. 多字节整数传输,需要转换字节序
  2. 单字节整数传输,不需要转换字节序
  3. 字符串传输,不需要转换字节序

IP地址

IP地址是主机在网络中的唯一标识,由两部分组成:网络号和主机号

![[IP地址理解.jpg]]

  • IP地址:
    • 网络号:标识计算机所属的网络
    • 主机号:标识该设备在该网络中的一个编号
  • 作用:网络传输过程中,给网络传输载体必须添加的信息,指定源IP地址和目标IP地址

分类

  • IPv4:使用4字节无符号整数表示,范围:\([0,2^{32}-1]\),\(10^9\)数量级,局域网扩充:让多个主机共享同一个IP地址
    • WAN(wide area network):广域网
    • LAN(local area network):局域网
  • IPv6:使用16字节无符号整数表示,范围:\([0,2^{128}-1]\),IPv6不兼容IPv4

划分

IPv4划分:由于IP地址较庞大,将其分为5类:A类,B类,C类,D类,E类网络

![[IPv4划分.jpg]]

网络类型 取值范围 网络号个数 主机号个数 用途
A类网络 1.0.0.0 -127.255.255.255 \(2^7\) \(2^{24}\) 已经保留不供使用
B类网络 128.0.0.0-191.255.255.255 \(2^{14}\) \(2^{16}\) 名地址网管中心
C类网络 192.0.0.0-223.255.255.255 \(2^{21}\) \(2^8\) 家庭,校园,公司使用
D类网络 224.0.0.0-239.255.255.255 组播IP
E类网络 240.0.0.0-255.255.255.255 保留或实验使用

特殊的IP地址

  • 网络号+每位全为0的主机号:表示该网络不分配给任何主机使用,例如:192.168.10.0
  • 网络号+每位全为1的主机号:表示当前网络的广播地址,也不分配给任何主机使用,例如:192.168.10.255
  • 网络号+主机号为1:默认表示网关,可以自定义网关IP
  • 127.0.0.0:本地环回IP,没有网络时用于测试当前主机IP
  • 0.0.0.0:表示当前局域网中的任意一个主机号
  • 255.255.255.255:一般表示广播地址

点分十进制

点分十进制:为了方便记忆,将IP地址中每一个字节单独计算出十进制数据,并用.进行分割,在程序中使用字符串来存储IP地址;网络传输时使用的是网络字节序的4字节无符号整数

转换函数

头文件:<sys/socket.h>,<netinet/in.h>,<arpa/inet.h>

  • in_addr_t inet_addr(const char *cp);:点分十进制的IP地址转化为网络字节序的4字节无符号整数
  • char *inet_ntoa(struct in_addr in);:网络字节序的4字节无符号整数转化为点分十进制的IP地址
  • const char* inet_ntop(int family,const void* src,char* dest,socklen_t size):将网络字节序的地址信息转化为点分十进制
    • 参数:
      • famliy:协议族
        • AF_INET:IPV4
        • AF_INET6:IPV6
      • src:指向sockaddr_in.sin_addr的地址
      • dest:存放点分十进制的缓冲区
      • size:缓冲区大小
    • 返回值:成功返回dest,失败返回nullptr并置位错误码
  • int inet_pton(int family,const char* restrict src,void* restrict dest):将点分十进制转化为网络字节序的地址信息
    • 返回值:成功返回1,src不包含合法的地址字符串返回0,famliy非法返回-1并置位错误码为EAFNOSUPPORT
#include <myHead.h>

int main(int argc, char const* argv[]) {
    const char* ip = "192.168.200.200";    //0x c0 a8 c8 c8
    unsigned int ip_net = inet_addr(ip);
    printf("ip_net:%x\n", ip_net);
    return 0;
}

端口号

端口号:为区分同一主机之间的每个应用程序,用端口号标识,是一个2字节的无符号整数,范围:\([0,65535]\)

不使用进程号原因:一个程序关闭再打开后就不是原来的进程

网络通信两个重要因素:IP地址,端口号

  • IP地址确定对端的主机
  • 端口号确定对端主机中指定的对端应用程序
  1. 0-1023:被特殊的应用程序已经占用了的
    • /etc/services文件中记录了特殊的端口号
    • TCP和UDP分别使用不同的一套标准
    • 有时使用特殊的端口号需要管理员权限
  2. 1024-4911:用户可分配的端口号
  3. 49152-65535:动态分配或系统自动分配的端口号

网络通信基础

套接字

  • 网络通信中,需要创建一个信息的载体进行数据的通信,即套接字(socket)

  • 调用socket()函数创建一个用于通信的套接字端点,并返回该端点对应的文件描述符

  • 通信端点中有两个缓冲区:发送缓冲区和接收缓冲区

  • 套接字通信原理:
    ![[套接字通信.jpg]]

  • int socket(int domain, int type, int protocol);:为通信创建套接字端点并返回文件描述符

    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 协议族
        • AF_UNIX,AF_LOCAL:本地通信,同一主机的多进程通信,具体内容在man 7 unix
        • AF_INET:提供了IPv4相关通信方式,具体内容在man 7 ip
        • PF_INET:等效于AF_INET
        • AF_INET6提供了IPv6相关通信方式,具体内容在man 7 ipv6
      • 通信方式,指定通信语义,多个用|隔开
        • SOCK_STREAM:支持TCP面向连接的通信协议
        • SOCK_DGRAM:支持UDP面向无连接的通信协议
        • SOCK_NONBLOCK:设置套接字位非阻塞模式
        • SOCK_CLOEXEC:设置close-on-exec,程序调用 exec 启动另一个程序时,该socket会自动关闭
        • IPPROTO_TCP:指定TCP协议
      • 通信协议:若参数2指定了特定协议,可以传0;若参数2有多个协议共同使用,需要明确指定当前套接字的通信协议
    • 返回值:成功返回文件描述符,失败返回-1并置位错误码
    • 使用完套接字需要用close关闭文件描述符
  • int socketpair(int domain, int type, int protocol, int sv[2]);:创建一对已连接的套接字,只支持AF_UNIX和AF_LOCAL,sv[0]读端,sv[1]写端

  • int shutdown(int sockfd, int how);:关闭套接字连接的一部分或全部

    • 参数:
      • 套接字
      • 关闭方式:
        • SHUT_RD:关闭接收
        • SHUT_WR:关闭发送
        • SHUT_RDWR:接收和发送都关闭
    • 返回值:成功返回0,失败返回-1并置位错误码
  • int getsockname(int sockfd,struct sockaddr* restrict addr,socklen_t* restrict addrlen):获取已经绑定了地址信息的套接字的地址信息

    • 返回值:成功返回0,失败返回-1并置位错误码
  • int getpeername(int sockfd,struct sockaddr* restrict addr,socklen_t* restrict addrlen):获取连接了sockfd的对端的地址信息

    • 返回值:成功返回0,失败返回-1并置位错误码

基于TCP的CS模型

网络通信过程中有两种通信方式:基于BS模型(浏览器服务器模型),基于CS模型(客户端服务器模型),这里是基于CS模型

通信原理:

![[TCP通信原理.jpg]]

bind

  • int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);:为套接字分配名称,即绑定IP地址和端口号
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 要绑定的套接字文件描述符
      • 通用地址信息结构体地址,对于不同的通信域而言,使用的实际结构体是不同的;传参时需要强转为该类型的结构体,防止编译器警告,具体的地址信息结构体:
        • AF_INETIPv4的通信方式:
          struct sockaddr_in {
             sa_family_t    sin_family;    //地址族,使用AF_INET
             in_port_t      sin_port;    //网络字节序的端口号
             struct in_addr sin_addr;    //网络地址
          };
          
          //网络地址
          struct in_addr {
             uint32_t       s_addr;    //网络字节序的IP地址
          };
          
          • s_addr:
            • INADDR_ANY:表示本机所有ipv4地址
            • INADDR_LOOPBACK:表示本地环回IP
        • AF_UNIX本地通信方式:
          struct sockaddr_un {
             sa_family_t sun_family;    //地址族,使用AF_UNIX
             char        sun_path[108];     //通信使用的文件,要求文件不存在
          };
          
      • 参数2结构体的大小
    • 返回值:成功返回0,失败返回-1并置位错误码

listen

  • int listen(int sockfd, int backlog);:将套接字设置为被动监听状态
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 套接字文件描述符
      • 挂起队列(未连接队列)能够增长到的最大长度,一般为128,SOMAXCONN表示系统允许的最大长度
    • 返回值:成功返回0,失败返回-1并置位错误码

accept

  • int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);:阻塞等待用户连接请求,如果已连接队列中有客户端,则从连接队列中取第一个并创建一个用于通信的套接字
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 服务器套接字文件描述符
      • 通用地址信息结构体地址,用于接收已连接的客户端套接字的地址信息,可以为nullptr
      • 存放参数2结构体的大小,可以为nullptr
    • 返回值:成功返回一个新的用于通信的套接字文件描述符,失败返回-1并置位错误码
    • 注意:accept阻塞等待客户端连接请求时,会先给用于通信的套接字分配一个目前未使用的文件描述符,即便停止阻塞时有更小的文件描述符也不使用了
  • int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);:可以添加socket标识
    • 参数:
      • flags:
        • SOCK_NONBLOCK:连接用套接字自动设置为非阻塞
        • SOCK_CLOEXEC:连接用的套接字自动设置close-on-exec
        • 多个用|隔开,可以为0(与acceptr一样)

recv

  • ssize_t recv(int sockfd, void *buf, size_t len, int flags);:从套接字中读取消息
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 通信的套接字文件描述符
      • 存放容器的起始地址
      • 要读取的数据大小
      • 读取标识
        • 0:阻塞接收
        • MSG_DONTWAIT:非阻塞接收
        • MSG_PEEK:接收消息但不移除,消息仍在缓冲区中
    • 返回值:
      • \(\gt 0\):接收到的字节数
      • \(=0\):表示对端已下线(对于TCP而言)
      • \(-1\):接收失败,置位错误码

send

  • ssize_t send(int sockfd, const void *buf, size_t len, int flags);:通过套接字发送信息
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 通信的套接字文件描述符
      • 要发送的数据的容器起始地址
      • 要发送的数据大小
      • 写入标识
        • 0:阻塞发送
        • MSG_DONTWAIT:非阻塞发送
    • 参数:成功返回发送的字节数,失败返回-1并置位错误码

sendfile

  • ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);:将文件从一个文件描述符拷贝到另一个文件描述符,内核内进行
    • 头文件:<sys/sendfile.h>
    • 参数:
      • 用于写的文件描述符
      • 用于读的文件描述符
      • 读文件开始拷贝的位置,若为nullptr则从文件开头开始
      • 要拷贝的文件大小
    • 返回值:成功返回拷贝的字节数,失败返回-1并置位错误码

connect

  • int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);:
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 用于通信的套接字文件描述符
      • 通用地址信息结构体,指定要连接的主机
      • 参数2结构体大小
    • 返回值:成功返回0,失败返回-1并置位错误码

实现

#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"

int main(int argc, char* argv[]) {
    //创建用于监听的套接字端口
    int sfd = socket(AF_INET, SOCK_STREAM, 0);     //用于监听的套接字
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success, sfd=%d\n", sfd);

    //绑定IP地址和端口号
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (bind(sfd, (const struct sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("bind error");
        return -1;
    }

    //启动监听
    if (listen(sfd, 128) == -1) {
        perror("listen error");
        return -1;
    }
    printf("start listen success\n");

    //阻塞等待客户端连接请求
    sockaddr_in cin;    //用于接收客户端的地址信息
    socklen_t socklen = sizeof(cin);
    int newfd = accept(sfd, (struct sockaddr*)(&cin), &socklen);    //用于通信的套接字
    if (newfd == -1) {
        perror("accept error");
        return -1;
    }
    printf("new client connected : [%s:%d]\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

    //数据收发
    char buf[128] = "";
    while (true) {
        memset(buf, 0, sizeof(buf));
        //接收消息
        int ret = recv(newfd, buf, sizeof(buf), 0);
        if (ret == 0) {
            printf("client is offline\n");
            break;
        } else if (ret == -1) {
            perror("recv error");
            return -1;
        }
        printf("received:  %s\n", buf);
        //发送消息
        strcat(buf, "   ^_^\n");
        if (send(newfd, buf, strlen(buf), 0) == -1) {
            perror("send error");
            return -1;
        }
        printf("send success\n");
    }

    //关闭套接字
    close(newfd);
    close(sfd);

    return 0;
}
#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"
#define CLI_PORT 9999
#define CLI_IP "192.168.200.200"

int main(int argc, char* argv[]) {
    //创建用于通信的套接字接口
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success, cfd=%d\n", cfd);

    //绑定IP地址和端口号,客户端可选,若不绑定就是随机的地址信息
    sockaddr_in cin;
    cin.sin_family = AF_INET;
    cin.sin_port = htons(CLI_PORT);
    cin.sin_addr.s_addr = inet_addr(CLI_IP);
    if (bind(cfd, (const sockaddr*)(&cin), sizeof(cin)) == -1) {
        perror("bind error");
        return -1;
    }
    printf("bind seccess\n");

    //连接服务器
    //注意地址信息要填充的是目标服务器的地址,而不是客户端的地址
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (connect(cfd, (const sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("connect error");
        return -1;
    }
    printf("connect service success\n");

    //数据收发
    char buf[128] = "";
    while (true) {
        memset(buf, 0, sizeof(buf));
        //发送消息
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0;
        if (send(cfd, buf, sizeof(buf), 0) == -1) {
            perror("send error");
            return -1;
        }
        printf("send success\n");
        //接收消息
        memset(buf, 0, sizeof(buf));
        int ret = recv(cfd, buf, sizeof(buf), 0);
        if (ret == 0) {
            printf("service is offline\n");
            break;
        } else if (ret == -1) {
            perror("recv error");
            return -1;
        }
        printf("received:  %s\n", buf);
    }

    //关闭套接字
    close(cfd);
    return 0;
}

基于UDP的CS模型

![[基于UDP的通信.jpg]]

recvfrom

  • ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);:从套接字中读取消息并读取对端地址信息
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 用于通信的套接字文件描述符
      • 用于接收信息的容器地址
      • 要接收的数据字节数
      • 是否阻塞
        • 0:阻塞
        • MSG_DONTWAIT:非阻塞
      • 接收对端地址信息的结构体地址
      • 参数5的结构体大小
    • 返回值:成功返回读取到的字节数,失败返回-1并置位错误码

sendto

  • ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);:通过套接字向指定地址发送信息
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 用于通信的套接字文件描述符
      • 用于发送信息的容器地址
      • 要发送的数据字节数
      • 是否阻塞
        • 0:阻塞
        • MSG_DONTWAIT:非阻塞
      • 对端的地址信息结构体地址
      • 参数5的结构体的大小
    • 返回值:成功返回发送的字节数,失败返回-1并置位错误码

实现

#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"

int main(int argc, char* argv[]) {
    //创建用于通信的socket端口
    int sfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success , sfd=%d\n", sfd);

    //绑定地址信息
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (bind(sfd, (const sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    //数据收发
    char buf[128] = "";
    sockaddr_in cin;
    socklen_t len = sizeof(cin);
    while (true) {
        memset(buf, 0, sizeof(buf));
        //接收信息
        if (recvfrom(sfd, buf, sizeof(buf), 0, (sockaddr*)(&cin), &len) == -1) {
            perror("recvfrom error");
            return -1;
        }
        printf("received from [%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);
        //发送信息
        strcat(buf, "  ^_^\n");
        if (sendto(sfd, buf, strlen(buf), 0, (const sockaddr*)(&cin), sizeof(cin)) == -1) {
            perror("sendto error");
            return -1;
        }
        printf("send to [%S:%d] success\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
    }
    return 0;
}
#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"
#define CLI_PORT 9999
#define CLI_IP "192.168.200.200"

int main(int argc, char* argv[]) {
    //创建用于通信的socket端口
    int cfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (cfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success , cfd=%d\n", cfd);

    //绑定地址信息,客户端可选,若不绑定为随机
    // sockaddr_in sin;
    // sin.sin_family = AF_INET;
    // sin.sin_port = htons(CLI_PORT);
    // sin.sin_addr.s_addr = inet_addr(CLI_IP);
    // if (bind(cfd, (const sockaddr*)(&sin), sizeof(sin)) == -1) {
    //     perror("bind error");
    //     return -1;
    // }
    // printf("bind success\n");

    //数据收发
    char buf[128] = "";
    sockaddr_in cin;
    cin.sin_family = AF_INET;
    cin.sin_port = htons(SER_PORT);
    cin.sin_addr.s_addr = inet_addr(SER_IP);
    while (true) {
        memset(buf, 0, sizeof(buf));
        //发送信息
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0;
        if (sendto(cfd, buf, strlen(buf), 0, (const sockaddr*)(&cin), sizeof(cin)) == -1) {
            perror("sendto error");
            return -1;
        }
        printf("send to [%s:%d] success\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
        //接收信息
        memset(buf, 0, sizeof(buf));
        if (recvfrom(cfd, buf, sizeof(buf), 0, nullptr, nullptr) == -1) {
            perror("recvfrom error");
            return -1;
        }
        printf("received from [%s:%d] : %s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);
    }

    //关闭套接字
    close(cfd);
    return 0;
}

TCP并发服务器

对于TCP通信方式,只能实现一个服务器端对应一个客户端,为了完成一个服务器端对应多个客户端,需要并发操作

sfd=socket();    //创建用于监听的套接字
bind();    //绑定地址信息
listen();    //开启监听
while(true){
	accept();    //阻塞接收客户端连接请求
	//数据收发
	recv();
	send();
	close();    //关闭用于通信的套接字
}
close();    //关闭用于监听的套接字

多进程实现

主进程完成对客户端的连接请求,子进程完成通信操作

signal(SIGCHLD,handler);    //绑定信号和处理方式

sfd=socket();    //创建用于监听的套接字
bind();    //绑定地址信息
listen();    //开启监听
while(true){
	accept();    //阻塞接收客户端连接请求
	pid=fork();    //创建子进程用于通信
	if(pid>0){    //父进程
		close(newfd);
	}
	else if(pid==0){
		close(sfd);
		//数据收发
		recv();
		send();
		close();    //关闭用于通信的套接字
		
		exit(EXIT_SUCCESS);
	}
	else {
		perror();
	}
}
close();    //关闭用于监听的套接字
#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"

int main(int argc, char* argv[]) {
    //绑定信号和处理方式,用于回收子进程资源
    auto handler = [](int signum)
        {
            if (signum == SIGCHLD) {
                //循环非阻塞方式回收僵尸进程
                while (waitpid(-1, nullptr, WNOHANG) > 0);
            }
        };
    if (signal(SIGCHLD, handler) == SIG_ERR) {
        perror("signal error");
        return -1;
    }

    //创建套接字端口
    int sfd = socket(AF_INET, SOCK_STREAM, 0);     //用于监听的套接字
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success, sfd=%d\n", sfd);

    //绑定IP地址和端口号
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (bind(sfd, (const struct sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("bind error");
        return -1;
    }

    //启动监听
    if (listen(sfd, 128) == -1) {
        perror("listen error");
        return -1;
    }
    printf("start listen success\n");

    sockaddr_in cin;    //用于接收客户端的地址信息
    socklen_t socklen = sizeof(cin);
    while (true) {
	    //阻塞等待客户端连接请求
        int newfd = accept(sfd, (struct sockaddr*)(&cin), &socklen);    //用于通信的套接字
        if (newfd == -1) {
            perror("accept error");
            return -1;
        }
        printf("new client connected : [%s:%d]\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));

        //创建子进程用于通信
        pid_t pid = fork();
        if (pid > 0) {    //父进程
            close(newfd);    //父进程中不对通信用的套接字操作
        } else if (pid == 0) {    //子进程
            close(sfd);    //子进程中不对监听用的套接字操作
            //数据收发
            char buf[128] = "";
            while (true) {
                memset(buf, 0, sizeof(buf));
                //接收消息
                int ret = recv(newfd, buf, sizeof(buf), 0);
                if (ret == 0) {
                    printf("client is offline\n");
                    break;
                } else if (ret == -1) {
                    perror("recv error");
                    return -1;
                }
                printf("received:  %s\n", buf);
                //发送消息
                strcat(buf, "   ^_^");
                if (send(newfd, buf, strlen(buf), 0) == -1) {
                    perror("send error");
                    return -1;
                }
                printf("send success\n");
            }

            //关闭套接字
            close(newfd);
            //退出子进程
            exit(EXIT_SUCCESS);
        } else {
            perror("fork error");
            return -1;
        }
    }

    close(sfd);

    return 0;
}

多线程实现

主线程完成对客户端的连接请求,分支线程完成通信操作

void* deal_cli_msg(void* arg){
	//数据收发
	recv();
	send();
	close();    //关闭用于通信的套接字
	//退出线程
	pthread_exit(nullptr);
}

sfd=socket();    //创建用于监听的套接字
bind();    //绑定地址信息
listen();    //开启监听
while(true){
	accept();    //阻塞接收客户端连接请求
	
	//创建分支线程
	pthread_create(&tid,nullptr,deal_cli_msg,&buf);
	//设置为分离态
	pthread_detach(tid);
}
close();    //关闭用于监听的套接字
#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"

struct data {
    int newfd;    //用于通信的套接字
    sockaddr_in cin;    //客户端地址信息
};

//分支线程
void* deal_cli_msg(void* arg) {
    int newfd = ((data*)arg)->newfd;
    sockaddr_in cin = ((data*)arg)->cin;

    //数据收发
    char buf[128] = "";
    while (true) {
        memset(buf, 0, sizeof(buf));
        //接收消息
        int ret = recv(newfd, buf, sizeof(buf), 0);
        if (ret == 0) {
            printf("client is offline\n");
            break;
        }
        else if (ret == -1) {
            perror("recv error");
            return nullptr;
        }
        printf("received:  %s\n", buf);
        //发送消息
        strcat(buf, "   ^_^");
        if (send(newfd, buf, strlen(buf), 0) == -1) {
            perror("send error");
            return nullptr;
        }
        printf("send success\n");
    }

    //关闭套接字
    close(newfd);

    //退出线程
    pthread_exit(nullptr);
}

int main(int argc, char* argv[]) {
    //创建套接字端口
    int sfd = socket(AF_INET, SOCK_STREAM, 0);     //用于监听的套接字
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success, sfd=%d\n", sfd);

    //绑定IP地址和端口号
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (bind(sfd, (const struct sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("bind error");
        return -1;
    }

    //启动监听
    if (listen(sfd, 128) == -1) {
        perror("listen error");
        return -1;
    }
    printf("start listen success\n");

    sockaddr_in cin;    //用于接收客户端的地址信息
    socklen_t socklen = sizeof(cin);
    while (true) {
        //阻塞等待客户端连接请求
        int newfd = accept(sfd, (struct sockaddr*)(&cin), &socklen);    //用于通信的套接字
        if (newfd == -1) {
            perror("accept error");
            return -1;
        }
        printf("new client connected : [%s:%d] newfd=%d\n",
            inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);

        //创建线程
        pthread_t tid = -1;
        data buf = { newfd,cin };
        if (pthread_create(&tid, nullptr, deal_cli_msg, &buf) != 0) {
            printf("pthread_create error\n");
            return -1;
        }
        //系统自动回收线程资源
        pthread_detach(tid);
    }


    close(sfd);

    return 0;
}

IO多路复用

当主机没有操作系统,或程序不能使用多进程或多线程完成并发操作,可以使用IO多路复用技术完成并发操作

有缓冲区的事件和阻塞函数的关系:

  • 事件先于函数发生,执行函数时不会阻塞
  • 事件后于函数发生,执行行函数时会阻塞

![[IO多路复用.jpg]]

select

接口

  • int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);:阻塞等待文件描述符集合中是否有相关事件产生,若有事件产生就解除阻塞,集合大小上限是1024
    • 头文件:<sys/select.h>
    • 参数:
      • 文件描述集合中最大的文件描述符+1
      • 读相关文件描述符集合的起始地址,不使用传nullptr
      • 写相关文件描述符集合的起始地址,不使用传nullptr
      • 异常处理相关文件描述符集合的起始地址,不使用传nullptr
      • 超时时间,传nullptr表示永久等待
        struct timeval {
           long    tv_sec;    //秒
           long    tv_usec;    //微秒
        };
        
        struct timespec {
           long    tv_sec;   //秒
           long    tv_nsec;    //纳秒
        };
        
    • 返回值:
      • \(\gt 0\):成功返回触发事件的文件描述符个数
      • \(=0\):超时仍未有事件发生
      • \(-1\):失败并置位错误码
    • 注意:解除阻塞后,文件描述符结合中只剩下本次触发了事件的文件描述符,其余的文件描述符被删除
  • void FD_CLR(int fd, fd_set *set);:将文件描述符fd从集合set中删除
  • int FD_ISSET(int fd, fd_set *set);:判断文件描述符fd是否在集合set中
  • void FD_SET(int fd, fd_set *set);:将文件描述符fd放入集合set中
  • void FD_ZERO(fd_set *set);:清空集合set

实现

sfd=socket();    //创建用于监听的套接字
bind();    //绑定地址信息
listen();    //开启监听
//定义文件描述符集合
fd_set fds,tmpfds;
FD_SERO(fds);
FD_SET(fds);
maxfd=sfd;

while(true){
	tmpfds=fds;    //备份
	select(maxfd,&fds,nullptr,nullptr,nullptr);
	if(FD_ISSET(sfd,&tmpfds)){
		newfd=accept();    //阻塞接收客户端连接请求
		FD_SET(newfd,&fds);    //newfd放入集合中
		//更新maxfd
	}
	for(int i=sfd+1;i<=maxfd;i++){
		if(FD_ISSET(i,&tmpfds)){
			//数据收发
			ret=recv();
			if(ret==0){    //客户端退出
				close();    //关闭用于通信的套接字
				FD_CLR();
				//更新maxfd
				continue;
			}
			send();
		}
	}
	close();    //关闭用于监听的套接字
}
#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"

int main(int argc, char* argv[]) {

    //创建套接字端口
    int sfd = socket(AF_INET, SOCK_STREAM, 0);     //用于监听的套接字
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success, sfd=%d\n", sfd);

    //绑定IP地址和端口号
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (bind(sfd, (const struct sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("bind error");
        return -1;
    }

    //启动监听
    if (listen(sfd, 128) == -1) {
        perror("listen error");
        return -1;
    }
    printf("start listen success\n");

    //定义文件描述符集合
    fd_set fds, tmpfds;
    FD_ZERO(&fds);
    FD_SET(sfd, &fds);
    int maxfd = sfd;    //集合中最大文件描述符

    sockaddr_in cin;    //用于接收客户端的地址信息
    socklen_t socklen = sizeof(cin);

    //客户端地址信息结数组
    sockaddr_in cin_arr[1024];
    socklen_t len_arr[1024];

    while (true) {
        //阻塞等待集合中相关的事件
        tmpfds = fds;
        int ret = select(maxfd + 1, &tmpfds, nullptr, nullptr, nullptr);
        if (ret == -1) {
            perror("select error");
            return -1;
        }
        else if (ret == 0) {
            printf("select timeout\n");
            return -1;
        }
        //检查集合中的文件描述符
        if (FD_ISSET(sfd, &tmpfds)) {    //用户连接请求事件触发
            sockaddr_in cin;
            socklen_t len;
            int newfd = accept(sfd, (struct sockaddr*)(&cin), &socklen);    //用于通信的套接字
            if (newfd == -1) {
                perror("accept error");
                return -1;
            }
            cin_arr[newfd] = cin;
            len_arr[newfd] = len;
            FD_SET(newfd, &fds);    //加入文件描述符集合
            maxfd = (maxfd < newfd) ? newfd : maxfd;    //更新maxfd
            printf("new client connected : [%s:%d]\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
        }
        for (int i = sfd + 1;i <= maxfd;i++) {    //遍历所有可能的客户端文件描述符,查看是否有事件
            if (FD_ISSET(i, &tmpfds)) {    //客户端有通讯事件
                //数据收发
                char buf[128] = "";
                //接收消息
                int ret = recv(i, buf, sizeof(buf), 0);
                if (ret == 0) {    //对端已下线
                    printf("client [%s:%d] is offline\n",
                        inet_ntoa(cin_arr[i].sin_addr), ntohs(cin_arr[i].sin_port));
                    close(i);    //关闭套接字
                    FD_CLR(i, &fds);    //将文件描述符从集合中移除
                    //更新maxfd
                    for (int j = maxfd;j >= sfd + 1;j--) {
                        if (FD_ISSET(j, &fds)) {
                            maxfd = j;
                            break;
                        }
                    }
                    continue;
                }
                else if (ret == -1) {
                    perror("recv error");
                    return -1;
                }
                printf("received from [%s:%d]:  %s\n",
                    inet_ntoa(cin_arr[i].sin_addr), ntohs(cin_arr[i].sin_port), buf);
                //发送消息
                strcat(buf, "   ^_^");
                if (send(i, buf, strlen(buf), 0) == -1) {
                    perror("send error");
                    return -1;
                }
                printf("send success\n");
            }
        }

    }

    close(sfd);

    return 0;
}

poll

接口

  • int poll(struct pollfd *fds, nfds_t nfds, int timeout);:阻塞等待文件描述符集合中是否有事件产生,返回本次触发事件的文件描述符个数
    • 头文件:<poll.h>
    • 参数:
      • 文件描述符集合的起始地址,是一个结构体数组
        struct pollfd {
           int   fd;    //文件描述符
           short events;    //要等待的事件,若该事件发生,events==revents
           short revents;   //实际发生的事件,调用函数结束后,内核自动设置
        };
        
        要等待的事件:
        • POLLIN:读事件
        • POLLOUT:写事件
      • 文件描述符结构体数组中元素个数
      • 超时时间:
        • \(\lt 0\):永久阻塞
        • \(=0\):非阻塞
        • \(\gt 0\):等待的毫秒数
    • 返回值:
      • \(\gt 0\):有事件触发的文件描述符个数
      • \(=0\):超时仍未有事件发生
      • \(-1\):失败并置位错误码

实现

cfd=socket();
bind();
connect();

//设置文件描述符集合
poll fds[2];
fds[0].fd=0;
fds[0].events=POLLIN;
fds[1].fd=cfd;
fds[1].events=POLLIN;

while(true){
	int res=poll();
	//标准输入事件
	if(fds[0].revents==fds[0].events){
		
	}
	//从服务器接收信息事件
	if(fds[1].revents==fds[1].events){
		
	}
}

close(cfd);

select和poll区别:

  1. select管理三类文件描述符集合,poll只管理一类,但可以操作很多事件
  2. select管理的文件描述符有上限,一般是1024个,poll管理文件描述符没有这个限制
  3. poll的效率比select的略高

epoll

eventpoll,内核实现IO多路复用的一种方式

epoll相对于select和poll的区别:

  1. select和poll基于线性结构,epoll基于红黑树,检测效率更高
  2. select和poll随着集合的扩大,检测效率会降低;epoll使用函数回调机制,效率较高,处理文件描述符的效率也不会随着集合的扩大而降低
  3. select和poll在工作过程中频繁在用户空间和内核空间之间拷贝文件描述符的数据,epoll在注册新的文件描述符或者修改文件描述符时,只需进行一次,能够有效减少数据在用户空间和内核空间之间的切换次数
  4. epoll没有最大文件描述符数量的限制
  5. select和poll需要对返回的文件描述符集合进行判断,才知道哪些文件描述符相关事件触发了,epoll可以直接得到已经就绪的文件描述符,无需再次检测
  6. 当多路复用比较频发进行,IO流量频繁的时候,一般不使用select和poll,使用epoll比较合适
  7. epoll只适用用于Linux,不能跨平台

接口

  • int epoll_create(int size);:创建一个epoll实例,返回实例的句柄,是一个文件描述符
    • 头文件:<sys/epoll.h>
    • 参数:epoll容器中最大节点个数,在Linux 2.6.8后可以忽略,但必须大于0
    • 返回值:成功返回控制epoll实例的文件描述符,失败返回-1并置位错误码
  • int epoll_create1(int flags):创建epoll实例,可以指定EPOLL_CLOSEXEC
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);:控制epoll实例
    • 头文件:<sys/epoll.h>
    • 参数:
      • epoll实例的文件描述符
      • 操作:
        • EPOLL_CTL_ADD:向epoll实例中添加新的文件描述符
        • EPOLL_CTL_MO:改变epoll实例中文件描述符要检测的事件
        • EPOLL_CTL_DEL:删除epoll实例中的文件描述符,此时参数4传nullptr
      • 要检测的文件描述符
      • 要检测的事件,是一个结构体的地址
        typedef union epoll_data {
           void        *ptr;    //解释性数据
           int          fd;    //文件描述符
           uint32_t     u32;
           uint64_t     u64;
        } epoll_data_t;
        
        struct epoll_event {
           uint32_t     events;    //要检测的事件
           epoll_data_t data;    //用户有效数据
        };
        
        要检测的事件:多个使用|隔开
        • EPOLLIN:读事件
        • EPOLLOUT:写事件,socket有写缓冲区空间可以继续写入时,会触发EPOLLOUT事件,若socket缓冲区满则不会触发,一般在写入全部数据后将其关闭
        • EPOLLERR:异常事件
        • EPOLLET:表示设置该事件为边缘触发模式
        • EPOLLONESHOT
        • EPOLLPRI:有紧急的数据可读
        • EPOLLRDHUP:对端关闭了写方向,但写缓冲区中可能还有数据
        • EPOLLHUP:连接断开
    • 成功返回文件描述符,失败返回-1并置位错误码
  • int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);:阻塞检测epoll实例中是否有文件描述符相关的事件发生
    • 头文件:<sys/epoll.h>
    • 参数:
      • epoll实例对应的文件描述符
      • 事件结构体数组,用于存放已触发的事件
      • 参数2数组的大小
      • 超时时间:
        • \(\lt 0\):永久阻塞
        • \(=0\):非阻塞
        • \(\gt 0\):等待的毫秒数
    • 返回值:
      • \(\gt 0\):有事件触发的文件描述符个数
      • \(=0\):超时仍未有事件发生
      • \(-1\):失败并置位错误码
        • EBADF:epfd是非法的文件描述符
        • EFAULT:events指向的内存不可访问
        • EINTR:被信号中断
        • EINVAL:epfd不是文件描述符

实现

sfd=socket();
bind();
listen();

//创建一个epoll实例,将sfd放入epoll实例中
int epfd=epoll_create();
epoll_event ee;
ee.events=EPOLLIN;
ee.data.fd=sfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&ee);

//定义事件数组用于接收触发的事件
epoll_event events[1024];
int size;

while(true){
	//阻塞等待文件描述符集合中的事件
	int num=epoll_wait();
	for(int i=0;i<num;i++){
		//有客户端发送连接请求
		if(events[i].fd==sfd){
			
		}
		//客户端有消息传来
		else {
			
		}
	}
}

close(sfd);
close(epfd);
#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"

int main(int argc, char* argv[]) {

    //创建套接字端口
    int sfd = socket(AF_INET, SOCK_STREAM, 0);     //用于监听的套接字
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success, sfd=%d\n", sfd);

    //绑定IP地址和端口号
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (bind(sfd, (const struct sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("bind error");
        return -1;
    }

    //启动监听
    if (listen(sfd, 128) == -1) {
        perror("listen error");
        return -1;
    }
    printf("start listen success\n");

    //定义文件描述符集合
    int epfd = epoll_create(1);
    if (epfd == -1) {
        perror("epoll_create error");
        return -1;
    }

    //将sfd相关事件放入epoll实例中
    epoll_event es;
    es.events = EPOLLIN;
    es.data.fd = sfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &es) == -1) {
        perror("epoll_ctl error");
        return -1;
    }

    //定义事件数组用于接收触发的事件
    epoll_event events[1024];
    int size = sizeof(events) / sizeof(events[0]);    //数组大小

    //客户端地址信息结数组
    sockaddr_in cin_arr[1024];
    socklen_t len_arr[1024];

    while (true) {
        //阻塞等待文件描述符集合中的事件
        int num = epoll_wait(epfd, events, size, -1);
        //遍历已触发的事件
        for (int i = 0;i < num;i++) {
            if (events[i].data.fd == sfd) {    //有客户端连接请求
                sockaddr_in cin;
                socklen_t len;
                int newfd = accept(sfd, (sockaddr*)(&cin), &len);
                if (newfd == -1) {
                    perror("accept error");
                    return -1;
                }
                cin_arr[newfd] = cin;
                len_arr[newfd] = len;
                //将新连接的客户端放入epoll实例中
                epoll_event ec;
                ec.events = EPOLLIN;
                ec.data.fd = newfd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ec) == -1) {
                    perror("epoll_ctl error");
                    return -1;
                }
                printf("new client connected : [%s:%d]\n",
                    inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
            }
            else {    //用于通信的套接字有客户端数据发来
                char buf[128] = "";
                int fd = events[i].data.fd;
                int res = recv(fd, buf, sizeof(buf), 0);
                if (res == 0) {    //对端已下线
                    printf("client [%s:%d] is offline\n",
                        inet_ntoa(cin_arr[fd].sin_addr), ntohs(cin_arr[fd].sin_port));
                    if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr) == -1) {
                        perror("epoll_ctl error");
                        return -1;
                    }
                    close(fd);
                    continue;
                }
                else if (res == -1) {
                    perror("recv error");
                    return -1;
                }
                printf("received from [%s:%d]:  %s\n",
                    inet_ntoa(cin_arr[fd].sin_addr), ntohs(cin_arr[fd].sin_port), buf);
                //发送消息
                strcat(buf, "   ^_^");
                if (send(fd, buf, strlen(buf), 0) == -1) {
                    perror("send error");
                    return -1;
                }
                printf("send success\n");
            }
        }
    }

    close(sfd);
    close(epfd);

    return 0;
}

工作模式

epoll工作模式有两种:水平触发和边缘触发,边缘触发效率更高

水平模式
  • 简称LT模式(level trigered),是默认的初始模式
  • LT模式下,当文件描述符准备就绪后,内核会通知哪些文件描述符就绪了,程序可以对就绪的文件描述符进行操作,若程序不做任何操作或者没有全部处理完该文件描述符的缓冲区中的信息,内核会继续通知,不会阻塞

特点:

  • 对于读事件:若该文件描述符的缓冲区中的数据没有被读取完,内核会继续解除阻塞,让程序继续处理,直到缓冲区中没有数据可以处理为止
  • 对于写事件:检测文件描述符缓冲区是否可用,若可用则解除阻塞,一般写文件描述符的缓冲区都是可用的
边缘模式
  • 简称ET模式(edge trigered),需要手动设置
  • ET模式下,当文件描述符准备就绪后,内核会通知哪些文件描述符就绪了,只通知一次,程序可以对就绪的文件描述符进行操作,若程序不做任何操作或者没有全部处理完该文件描述符的缓冲区中的信息,内核不会再通知,直到下次事件再触发

使用边缘模式一般需要将文件描述符设置为非阻塞,while(true)调用函数处理缓冲区中的数据,直到EAGAIN或EWOULDBLOCK暂时没有数据退出循环

特点:

  • 对于读事件:若该文件描述符的缓冲区中的数据没有被读取完,内核不会继续解除阻塞,直到下次读事件再触发,但下一次读事件读取的是上次没处理完的内容
  • 对于写事件:检测文件描述符缓冲区是否可用,若可用则解除阻塞,一般写文件描述符的缓冲区都是可用的
EPOLLONESHOT

一个线程处理EPOLLONESHOT事件时,其他线程无法处理,事件处理完后需要用epoll_ctl的EPOLL_CTL_MO操作重置EPOLLSHOT事件

组播和广播

网络属性

网络套接字在不同层中有不同的设置:应用层(套接字层),传输层,网络层,以太网层(网络接口层)

  • int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);:设置套接字属性
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 套接字文件描述符
      • 要操作的层:
        • SOL_SOCKET:应用层,属性在man 7 socket中查找,常用属性:
          • SO_REUSEADDR:地址快速复用
          • SO_REUSEPORT:端口号快速复用
          • SO_BROADCAST:允许广播
          • SO_RCVTIMEO:接收超时时间
          • SO_SNDTIMEO:发送超时时间
          • SO_LINGER:等待缓冲区中的数据处理完
            struct linger {
            	int l_onoff; // 1启用LINGER,0关闭LINGER
            	int l_linger; // 等待时间,单位:秒 
            };
            
          • SO_KEEPALIVE:启用keepalive机制,连接长时间空闲系统会发送TCP探测包
        • IPPROTO_TCP:传输层基于TCP的协议,属性在man 7 tcp中查找
          • TCP_NODELAY:是否要禁用Nagle算法,禁用后小数据包能更快发送,Nagle算法允许小数据包合并
        • IPPROTO_UDP:传输层基于UDP的协议,属性在man 7 udp中查找
        • IPPROTO_IP:网络层,属性在man 7 ip中查找,常用属性
          • IP_ADD_MEMBERSHIP:加入多播组
            属性值为结构体:
            struct ip_mreqn {
               struct in_addr imr_multiaddr;    //组播IP结构体,内部的IP是网络字节序
               struct in_addr imr_address;    //接收端IP结构体,内部的IP是网络字节序
               int            imr_ifindex;    //网卡设备编号
            };
            
            网卡设备编号用ip ad查看,一般为2
      • 对应层中的属性名
      • 参数3属性的值的地址,一般为int*类型
      • 参数4值的大小
    • 返回值:成功返回0,失败返回-1并置位错误码
  • int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen):获取套接字属性
    • 头文件:
      • <sys/types.h>
      • <sys/socket.h>
    • 参数:
      • 套接字文件描述符
      • 要操作的层
        • SOL_SOCKET
          • SO_ERROR:查询socket上的错误
      • 对应层中的属性名
      • 用于接收参数3属性的值,一般为int类型
      • 参数4值的大小
    • 返回值:成功返回0,失败返回-1并置位错误码
#include <myHead.h>

#define SER_PORT 8888
#define SER_IP "192.168.200.200"

int main(int argc, char* argv[]) {
    //创建套接字端口
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket success, fd=%d\n", fd);

    //查询SO_REUSEADDR属性
    int reuse = -1;
    socklen_t len = sizeof(reuse);
    if (getsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, &len) == -1) {
        perror("getsockopt error");
        return -1;
    }
    printf("reuse=%d\n", reuse);

    //设置SO_REUSEADDR属性为允许
    reuse = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, len) == -1) {
        perror("setsockopt error");
        return -1;
    }

    //查询SO_REUSEADDR属性
    len = sizeof(reuse);
    if (getsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, &len) == -1) {
        perror("getsockopt error");
        return -1;
    }
    printf("reuse=%d\n", reuse);

    //绑定IP地址和端口号
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SER_PORT);
    sin.sin_addr.s_addr = inet_addr(SER_IP);
    if (bind(fd, (const struct sockaddr*)(&sin), sizeof(sin)) == -1) {
        perror("bind error");
        return -1;
    }


    while (true);
    close(fd);
    return 0;
}

广播

  • 主机之间是一对多的通信关系,网络对其中每一台主机的消息都会转发
  • 当前网络下的所有主机都可以收到该消息,无论是否愿意接收消息
  • 基于UDP面向无连接的通信方式
  • 广播分为发送端和接收端
  • 对于发送端,需要设置允许广播,并向广播地址发送数据
    • 广播地址:网络号+主机号每位全为1,可以通过ifconfig查看
  • 对于接收端,只要绑定了广播地址,都可以接收到发送端的数据
  • 广播消息不能穿过路由器到主干路由上

发送端

发送端需要设置允许广播

socket();    //创建套接字
setsockopt();    //设置套接字,允许广播
bind();    //绑定地址信息,可选
//发送数据到广播,目标地址IP是广播IP,端口与接收端一致
sendto();
close();    //关闭套接字
#include <myHead.h>

using namespace std;

#define PORT 8888
#define BIP "192.168.200.255"

int main(int argc, char* argv[]) {
    //创建发送信息用的套接字
    int sndfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sndfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket successs, sndfd=%d\n", sndfd);

    //设置套接字属性,允许广播
    int broadcast = 1;
    if (setsockopt(sndfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)) == -1) {
        perror("setsockopt error");
        return -1;
    }
    printf("set broadcast success\n");

    //发送数据
    //接收端是广播IP
    sockaddr_in rin;
    rin.sin_family = AF_INET;
    rin.sin_port = htons(PORT);
    rin.sin_addr.s_addr = inet_addr(BIP);
    char buf[128] = "";
    while (true) {
        memset(buf, 0, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0;
        if (sendto(sndfd, buf, strlen(buf), 0, (const sockaddr*)(&rin), sizeof(rin)) == -1) {
            perror("sendto error");
            return -1;
        }
        printf("send to broadcast success\n");
    }

    close(sndfd);
    return 0;
}

接收端

socket();    //创建套接字
bind();    //绑定地址信息,绑定地址信息IP是广播IP,端口与发送端一致
//从发送端接收数据
recvfrom();
close();    //关闭套接字
#include <myHead.h>

using namespace std;

#define PORT 8888
#define BIP "192.168.200.255"

int main(int argc, char* argv[]) {
    //创建接收信息用的套接字
    int recvfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (recvfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket successs, recvfd=%d\n", recvfd);

    //绑定地址信息
    sockaddr_in rin;
    rin.sin_family = AF_INET;
    rin.sin_port = htons(PORT);
    rin.sin_addr.s_addr = inet_addr(BIP);
    if (bind(recvfd, (const sockaddr*)(&rin), sizeof(rin)) == -1) {
        perror("bind error");
        return -1;
    }

    //接收数据
    char buf[128] = "";
    sockaddr_in sin;
    socklen_t len;
    while (true) {
        memset(buf, 0, sizeof(buf));
        if (recvfrom(recvfd, buf, sizeof(buf), 0, (sockaddr*)(&sin), &len) == -1) {
            perror("recvfd error");
            return -1;
        }
        printf("received from [%s:%d] : %s\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), buf);
    }

    close(recvfd);
    return 0;
}

组播

  • 广播是给同一个网络下的所有主机发送消息,会占用大量的网络带宽,影响正常网络通信,造成网络拥塞
  • 组播也是实现一对多的通信机制,加入同一个多播组的成员都能收到组播消息
  • 基于UDP面向无连接的通信方式
  • 同一个多播组的所有主机都可以收到该消息,无论是否愿意接收消息
  • 组播地址:D类网络地址 (224.0.0.0 - 239.255.255.255)

发送端

socket();    //创建用于发送信息的套接字
bind();    //绑定地址信息,可选
//发送数据到组播IP,目标地址是一个D类网络,端口和接收端保持一致
sendto();
close();    //关闭套接字
#include <myHead.h>

using namespace std;

#define PORT 8888
#define GIP "224.1.2.3"

int main(int argc, char* argv[]) {
    //创建发送信息用的套接字
    int sndfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sndfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket successs, sndfd=%d\n", sndfd);

    //发送数据
    //接收端是组播IP
    sockaddr_in rin;
    rin.sin_family = AF_INET;
    rin.sin_port = htons(PORT);
    rin.sin_addr.s_addr = inet_addr(GIP);
    char buf[128] = "";
    while (true) {
        memset(buf, 0, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0;
        if (sendto(sndfd, buf, strlen(buf), 0, (const sockaddr*)(&rin), sizeof(rin)) == -1) {
            perror("sendto error");
            return -1;
        }
        printf("send to group success\n");
    }

    close(sndfd);
    return 0;
}

接收端

socket();    //创建用于接收消息的套接字
setsockopt();    //设置套接字,加入多播组
bind();    //绑定地址信息,绑定IP地址是组播IP,端口和发送端一致
//接收数据
recvfrom();
close();    //关闭套接字
#include <myHead.h>

using namespace std;

#define PORT 8888
#define GIP "224.1.2.3"
#define LOCALIP "192.168.200.200"

int main(int argc, char* argv[]) {
    //创建接收信息用的套接字
    int recvfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (recvfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("socket successs, recvfd=%d\n", recvfd);

    //设置套接字,加入多播组
    ip_mreq imr;
    imr.imr_multiaddr.s_addr = inet_addr(GIP);
    imr.imr_interface.s_addr = inet_addr(LOCALIP);
    if (setsockopt(recvfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1) {
        perror("setsockopt error");
        return -1;
    }
    printf("join membership seccess\n");

    //绑定地址信息
    sockaddr_in rin;
    rin.sin_family = AF_INET;
    rin.sin_port = htons(PORT);
    rin.sin_addr.s_addr = inet_addr(GIP);
    if (bind(recvfd, (const sockaddr*)(&rin), sizeof(rin)) == -1) {
        perror("bind error");
        return -1;
    }

    //接收数据
    char buf[128] = "";
    sockaddr_in sin;
    socklen_t len;
    while (true) {
        memset(buf, 0, sizeof(buf));
        if (recvfrom(recvfd, buf, sizeof(buf), 0, (sockaddr*)(&sin), &len) == -1) {
            perror("recvfd error");
            return -1;
        }
        printf("received from [%s:%d] : %s\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), buf);
    }

    close(recvfd);
    return 0;
}

域套接字

  • 域套接字是原始的套接字通信方式,和IPC通信方式一样,属于同一主机内多个进程间通信方式
  • 由于不需要进程跨主机通信,不需要绑定IP地址和端口号,需要绑定套接字文件
  • 域套接字通信载体为套接字文件
  • 使用内核空间完成数据传输
  • 域套接字分为流式域套接字和报式域套接字

流式域套接字

创建套接字使用SOCK_STREAM通信方式

若没有绑定地址信息结构体,系统会给一个随机文件,文件名是一个乱码

#include <myHead.h>

using namespace std;

struct sockaddr_un {
    sa_family_t sun_family;    //地址族,使用AF_UNIX
    char        sun_path[108];     //通信使用的文件
};

int main() {
    //创建用于通信的套接字
    int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("sucket success\n");

    //判断套接字文件是否存在
    if (access("./unix", F_OK) == 0) {
        if (unlink("./unix") == -1) {    //存在就删除
            perror("unlink error");
            return -1;
        }
    }

    //绑定套接字文件
    sockaddr_un sun;
    sun.sun_family = AF_UNIX;
    strcpy(sun.sun_path, "./unix");
    if (bind(sfd, (const sockaddr*)(&sun), sizeof(sun)) == -1) {
        perror("bind error");
        return -1;
    }

    //启动监听
    if (listen(sfd, 128) == -1) {
        perror("listen error");
        return -1;
    }

    //阻塞等待客户端连接请求
    sockaddr_un cun;
    socklen_t socklen = sizeof(cun);
    int newfd = accept(sfd, (sockaddr*)(&cun), &socklen);
    if (newfd == -1) {
        perror("accept error");
        return -1;
    }
    printf("connected to [%s]\n", cun.sun_path);

    //数据收发
    char buf[128] = "";
    while (true) {
        memset(buf, 0, sizeof(buf));
        int res = recv(newfd, buf, sizeof(buf), 0);
        if (res == -1) {
            perror("recv error");
            return -1;
        }
        if (res == 0) {
            printf("client is offline\n");
            break;
        }
        printf("received : %s\n", buf);
        strcat(buf, "  ^_^\n");
        if (send(newfd, buf, strlen(buf), 0) == -1) {
            perror("send error");
            return -1;
        }
        printf("send success\n");
    }

    close(sfd);
    close(newfd);
    return 0;
}
#include <myHead.h>

using namespace std;

struct sockaddr_un {
    sa_family_t sun_family;    //地址族,使用AF_UNIX
    char        sun_path[108];     //通信使用的文件
};

int main() {
    //创建用于通信的套接字
    int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (cfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("sucket success\n");

    //判断套接字文件是否存在
    if (access("./linux", F_OK) == 0) {
        if (unlink("./linux") == -1) {    //存在就删除
            perror("unlink error");
            return -1;
        }
    }

    //绑定套接字文件
    sockaddr_un cun;
    cun.sun_family = AF_UNIX;
    strcpy(cun.sun_path, "./linux");
    if (bind(cfd, (const sockaddr*)(&cun), sizeof(cun)) == -1) {
        perror("bind error");
        return -1;
    }

    //连接服务器
    sockaddr_un sun;
    sun.sun_family = AF_UNIX;
    strcpy(sun.sun_path, "./unix");
    if (connect(cfd, (const sockaddr*)(&sun), sizeof(sun)) == -1) {
        perror("connect error");
        return -1;
    }
    printf("connected\n");

    //数据收发
    char buf[128] = "";
    while (true) {
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0;
        if (send(cfd, buf, sizeof(buf), 0) == -1) {
            perror("send error");
            return -1;
        }
        printf("send success\n");
        memset(buf, 0, sizeof(buf));
        if (recv(cfd, buf, sizeof(buf), 0) == -1) {
            perror("recv error");
            return -1;
        }
        printf("received : %s\n", buf);
    }

    close(cfd);
    return 0;
}

报式域套接字

创建套接字使用SOCK_DGRAM通信方式

#include <myHead.h>

using namespace std;

struct sockaddr_un {
    sa_family_t sun_family;    //地址族,使用AF_UNIX
    char        sun_path[108];     //通信使用的文件
};

int main() {
    //创建用于通信的套接字
    int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("sucket success\n");

    //判断套接字文件是否存在
    if (access("./unix", F_OK) == 0) {
        if (unlink("./unix") == -1) {    //存在就删除
            perror("unlink error");
            return -1;
        }
    }

    //绑定套接字文件
    sockaddr_un sun;
    sun.sun_family = AF_UNIX;
    strcpy(sun.sun_path, "./unix");
    if (bind(sfd, (const sockaddr*)(&sun), sizeof(sun)) == -1) {
        perror("bind error");
        return -1;
    }

    //数据收发
    sockaddr_un cun;
    socklen_t socklen = sizeof(cun);
    char buf[128] = "";
    while (true) {
        memset(buf, 0, sizeof(buf));
        if (recvfrom(sfd, buf, sizeof(buf), 0, (sockaddr*)(&cun), &socklen) == -1) {
            perror("recvfrom error");
            return -1;
        }
        printf("received from [%s]: %s\n", cun.sun_path, buf);
        strcat(buf, "  ^_^\n");
        if (sendto(sfd, buf, strlen(buf), 0, (const sockaddr*)(&cun), sizeof(cun)) == -1) {
            perror("sendto error");
            return -1;
        }
        printf("send to [%s] success\n", cun.sun_path);
    }

    close(sfd);

    return 0;
}
#include <myHead.h>

using namespace std;

struct sockaddr_un {
    sa_family_t sun_family;    //地址族,使用AF_UNIX
    char        sun_path[108];     //通信使用的文件
};

int main() {
    //创建用于通信的套接字
    int cfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (cfd == -1) {
        perror("socket error");
        return -1;
    }
    printf("sucket success\n");

    //判断套接字文件是否存在
    if (access("./linux", F_OK) == 0) {
        if (unlink("./linux") == -1) {    //存在就删除
            perror("unlink error");
            return -1;
        }
    }

    //绑定套接字文件
    sockaddr_un cun;
    cun.sun_family = AF_UNIX;
    strcpy(cun.sun_path, "./linux");
    if (bind(cfd, (const sockaddr*)(&cun), sizeof(cun)) == -1) {
        perror("bind error");
        return -1;
    }

    //数据收发
    sockaddr_un sun;
    sun.sun_family = AF_UNIX;
    strcpy(sun.sun_path, "./unix");
    socklen_t socklen = sizeof(sun);
    char buf[128] = "";
    while (true) {
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0;
        if (sendto(cfd, buf, strlen(buf), 0, (const sockaddr*)(&sun), sizeof(sun)) == -1) {
            perror("sendto error");
            return -1;
        }
        printf("send to [%s] success\n", sun.sun_path);
        memset(buf, 0, sizeof(buf));
        if (recvfrom(cfd, buf, sizeof(buf), 0, (sockaddr*)(&sun), &socklen) == -1) {
            perror("recvfrom error");
            return -1;
        }
        printf("received from [%s] : %s\n", sun.sun_path, buf);
    }

    close(cfd);

    return 0;
}
posted @ 2026-10-07 16:16  mofei1116  阅读(6)  评论(0)    收藏  举报