c/c++:网络通信基础socket(网络设计模式、字节序、IP地址转换、sockaddr数据结构、套接字函数、TCP通信流程)

目录

1. 概念

1.1 网络设计模式

  - B/S

  - C/S

- IP和端口

- OSI/ISO 网络分层模型

2. 协议格式

3. socket编程

3.1 字节序

- 接口转换函数

3.2 IP地址转换

3.3 sockaddr数据结构

3.4 套接字函数

4. TCP通信流程

tcp 服务器server通信操作流程:

tcp 客户端client通信操作流程:

 
1. 概念
1.1 网络设计模式
  - B/S

    - 客户端: 浏览器
    - 服务器: 服务器

    优势: 跨平台, 开发成本低

    劣势:

    ​    是的协议的固定的: http, https

    ​    不能处理大的数据
  - C/S

    - 客户端: 桌面应用程序
    - 服务器: 后台服务器

     优势: 可以处理大量的磁盘数据

     劣势: 如果跨平台, 需要重新开发, 成本高

 
- IP和端口

      - IP地址

    - IPV4

      - 实际是一个32位的整形数 -> 本质 -> 4字节   int a;
      - 我们看的的不是这个整形数, 点分十进制字符串 -> 192.168.247.135
        - 分成了4份, 每份1字节, 8bit   ->  char , 最大值为 255  -> 最大取值: 255.255.255.255
      - IP可以有多少个  2^32^ - 1 个

    - IPV6

      - 实际是一个128位的整形数
      - xxx:xxx:xxx:xxx:xxx:xxx:xxx:xxx ,  分成了8分, 每份16位 -> 每一部分以16进制的方式表示
      - IP可以有多少个  2^128^ - 1 个
    - IP地址的作用:
      - 通过IP地址能够找到某一台主机

      - 端口

    - 在一个主机上运行着很多进程
    - 将数据发送到某台主机上的某个进程
    - 如果要进程网络通信, 可以让这个进程绑定一个端口
      - 通过这个端口就可以确定某个进程
    - 端口号: unsigned short int   ->   16位
      - 端口取值范围: 0 -65535    (2^16^)

 
- OSI/ISO 网络分层模型

  > OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织组织)在1985年研究的网络互联模型。

      - 七层模型

        底层 --------->上层
        物 数 网 传 会 表 应

            > - 物理层:

        >   - 物理层负责最后将信息编码成电流脉冲或其它信号用于网上传输

            > - 数据链路层:  

        >   - 数据链路层通过物理网络链路供数据传输。
        >   - 规定了0和1的分包形式,确定了网络数据包的形式;

            > - 网络层

        >   - 网络层负责在源和终点之间建立连接;
        >   - 此处需要确定计算机的位置,怎么确定?IPv4,IPv6

            > - 传输层

        >   - 传输层向高层提供可靠的端到端的网络数据流服务。
        >   - 每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信

            > - 会话层

        >   - 会话层建立、管理和终止表示层与实体之间的通信会话;
        >   - 建立一个连接(自动的手机信息、自动的网络寻址);

            > - 表示层:

        >   - 对应用层数据编码和转化, 确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;
        >   - 可以理解为:解决不同系统之间的通信,eg:手机上的QQ和Windows上的QQ可以通信;

            > - 应用层:

        >   - 规定数据的传输协议

    四层模型

 

 
2. 协议格式

 
3. socket编程

    // 套接字通信分两部分:
     - 服务器端: 被动接受连接的角色, 不会主动发起连接
     - 客户端通信: 主动向服务器发起连接
     
     socket是一套通信接口, 下linux和windows都可以使用, 但是有细微差别

 
3.1 字节序

    字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)

    - 概念

  - Little-Endian -> 主机字节序
    - 有一个数据: 0x12345678, 在内存中进行存储
    - 内存的低地址位存储数据低位字节, 内存高地址位存储数据的高位字节
  - Big-Endian -> 网络字节序
    - 内存的低地址位存储数据高位字节, 内存的高地址位存储数据的低位字节

    - 字节序举例

    // 使用16进制在内存中表示这两个数,即:
          - 0x12 34 56 78   -> 四字节   char -> 255 -> ff  
            - 0x11223344   -> 四字节

    - 小端

        低地址位 -------------> 高地址位
        0x78     0x56    0x34    0x12
        0x44    0x33    0x22    0x11

    - 大端

        低地址位 -------------> 高地址位
        0x12    0x34    0x56    0x78
        0x11    0x22    0x33    0x44

    - 接口转换函数

BSD Socket提供了封装好的转换接口,方便程序员使用。

从主机字节序(h)到网络字节序(n)的转换函数:htons、htonl;

从网络字节序(n)到主机字节序(h)的转换函数:ntohs、ntohl。

    #include <arpa/inet.h>
      // shot int -> 4字节(64位)
      // h -> host
      // n -> network
      // s -> short
      // l -> long
      // xtoxs() -> 进行端口转换
      uint16_t htons(uint16_t hostshort);
          参数: 主机字节数的short型数值 -> 要转换的数(主机)
          返回值: 转换之后得到是数据 (网络字节序)
      uint16_t ntohs(uint16_t netshort);
     
      // long -> 8字节(64位)
      // xtoxl() -> 进行IP转换
      uint32_t htonl(uint32_t hostlong);
      uint32_t ntohl(uint32_t netlong);

 
3.2 IP地址转换

    #include <arpa/inet.h>
    // 字符串: 192.168.1.100 (点分十进制字符串)
    // p -> 点分十进制字符串 IP
    // n -> network
    // 将主机字节序的 字符串IP -> 网络字节序的 整形数
    int inet_pton(int af, const char *src, void *dst);
        参数:
            - af: 地址族协议, ipv4, ipv6
                ipv4: AF_INET, ipv6:AF_INET6
            - src: 点分十进制字符串 IP
            - dst: 传出参数, 执行一块内存的地址, 将转换得到的网络字节序的整形数存储到这块内存中
        返回值:
            -1: 失败
            1: 成功
            0: 查字典
     
    // 网络字节序的整形IP -> 点分十进制字符串 IP
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
        参数:
            - af: 地址族协议, ipv4, ipv6
                ipv4: AF_INET, ipv6:AF_INET6
            - src: 指向要转换的 网络字节序的整形IP 地址
            - dst: 转换成功之后的 点分十进制字符串 存储的位置
            - size: 修饰的就是第三个参数 dst 对应的内存大小
        返回值:
            NULL: 失败
            非空指针, 指向第三个三种指针的内存: 成功

 
3.3 sockaddr数据结构

    结构体 sockaddr、sockaddr_in用于网络通信

    结构体 sockaddr_un用于进程间通信

    结构体 sockaddr_in用于ipv6通信

    由于结构体sockaddr需要用指针偏移添加IP地址,这样很麻烦,在实际中我们使用sockaddr_in来添加端口号、IP地址。再强转成sockaddr类型,因为这2个结构体大小一样,后面的服务器—客户端程序会有具体体现。

    struct sockaddr {
        sa_family_t sa_family;    // 地址族协议, ipv4, ipv6
        char        sa_data[14];
    }
     
    struct sockaddr_in
    {
        sa_family_t sin_family;        IP选择AF_INET(ipv4)、AF_INET6(ipv6)
        in_port_t sin_port;         端口(网络字节序:htons() )
        struct in_addr sin_addr;    IP地址(网络字节序:inet_pton() )
        //预留空间:
        unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
                   sizeof (in_port_t) - sizeof (struct in_addr)];  
    };
     
    struct in_addr     
    {
        in_addr_t s_addr;        IP地址(网络字节序:inet_pton() )
    };  
     
    typedef unsigned short  uint16_t;
    typedef unsigned int    uint32_t;
    typedef uint16_t in_port_t;
    typedef uint32_t in_addr_t;
    typedef unsigned short int sa_family_t;
    #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
     

 
3.4 套接字函数

    #include <arpa/inet.h>    
    // 创建一个套接字
    int socket(int domain, int type, int protocol);
        参数:
            - domain: 地址族协议
                AF_INET: ipv4
                AF_INET6: ipv6
                AF_UNIX, AF_LOCAL: 进行本地套接字通信(进程间通信)
            - type: 通信过程中使用的协议
                SOCK_STREAM: 流式协议
                SOCK_DGRAM: 报式协议
            - protocol: 一般写0
                - SOCK_STREAM: 流式协议默认使用使用: tcp
                - SOCK_DGRAM: 报式协议默认使用使用: udp
        返回值: 这个文件描述符操作的是内核缓冲区
            成功: 文件描述符 > 0
            失败: -1
     
    // 绑定函数 -> 将fd 和本地的 IP + Port进程绑定
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
        参数:
            - sockfd: 通过socket函数得到的
            - addr: 需要将IP和Port初始化到这个结构体中
                   - addrlen: 第二个参数结构体占的内存大小
     
    // 设置监听
    int listen(int sockfd, int backlog);    // /proc/sys/net/core/somaxconn
        参数:
            - sockfd: 通过socket函数得到的
            - backlog: 已经连接成功, 但是还没有被处理的连接指定的数值不能大于/proc/sys/net/core/somaxconn 中存储的数据, 默认为128
     
    // 默认是一个阻塞函数, 阻塞等待客户端请求。请求到达, 接收客户端连接,得到一个用于通信的文件描述符
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
        参数:
            - sockfd: 用于监听的文件描述符(套接字)
                - addr: 传出参数, 记录了连接成功的客户端的IP和端口信息
                - addrlen: 第二个参数结构体对应的内存大小
        返回值:
            - 成功: 通信的文件描述符 > 0
            - 失败: -1
                
    // 客户端使用该函数连接服务器
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
        参数:
            - sockfd: 用于通信的文件描述符
            - addr: 客户端要连接的服务器的地址信息
            - addrlen: 第二个参数结构体占的内存大小
        返回值:
            连接成功: 0
            连接失败: -1
                
     // 写数据
    ssize_t write(int fd, const void *buf, size_t count);
     // 读数据
    ssize_t read(int fd, void *buf, size_t count);

 
4. TCP通信流程

    // tcp / udp-> 传输层协议
    tcp: 面向连接的, 安全的, 流式传输协议
        - 安全: 不会丢数据
    udp: 面向无连接的, 不安全, 报式传输协议

    tcp 服务器通信操作流程:

    1. 创建一个用于监听的套接字
        - 监听: 监听有客户的连接
        - 套接字: 这个套接字是一个文件描述符
    2. 将这个监听文件描述符和本地的IP和端口绑定  (IP和端口 == 服务器地址信息)
        - 客户端连接服务器的时候使用的就是这个IP和端口
    3. 设置监听, 监听的fd开始工作
    4. 阻塞等待, 当有客户端发起连接, 解除阻塞, 接受客户端的连接, 会得到一个用户通信的套接字(fd)
    5. 通信
        - 接收数据
        - 发送数据
    6. 通信结束, 断开连接

    tcp 客户端的通信流程:

    1. 创建一个用于通信的套接字 (fd)
    2. 连接服务器, 需要指定连接的服务器的 IP 和 Port
    3. 连接成功, 客户端可以直接和服务通信
        - 接收数据
        - 发送数据
    4. 断开连接

tcp 服务器server通信操作流程:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <arpa/inet.h>
     
    int main()
    {
        // 1.创建用于监听的套接字
        int fd = socket(AF_INET, SOCK_STREAM, 0);
        if (fd == -1)
        {
            perror("socket");
            exit(0);
        }
     
        // 2.绑定
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;            //ipv4
        addr.sin_addr.s_addr = INADDR_ANY;    //获取IP的操作交给了内核
        // 上面的代码等价于:inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
        addr.sin_port = htons(8989);          //端口
        int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
        if (ret == -1)
        {
            perror("bind");
            exit(0);
        }
     
        // 3.设置监听
        int lis_ret = listen(fd, 100);
        if (lis_ret == -1)
        {
            perror("listen");
            exit(0);
        }
     
        // 4.等待被连接
        struct sockaddr_in addr_cli;
        int len = sizeof(addr_cli);
        int connfd = accept(fd, (struct sockaddr*)&addr_cli, &len);
        if (connfd == -1)
        {
            perror("accept");
            exit(0);
        }
     
        // 通讯
        while (1)
        {
            // 读数据
            char recvBuf[1024];
            read(connfd, recvBuf, sizeof(recvBuf));
            printf("recv buf : %s\n", recvBuf);
            // 写数据
            write(connfd, recvBuf, strlen(recvBuf));
        }
        
        //释放
        close(fd);
        close(connfd);
     
        return 0;
    }

 
tcp 客户端client通信操作流程:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <arpa/inet.h>
     
    int main()
    {
        // 1. 创建用于通信的套接字
        int fd = socket(AF_INET, SOCK_STREAM, 0);
        if(fd == -1)
        {
            perror("socket");
            exit(0);
        }
     
        // 2. 连接服务器
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;  // ipv4
        addr.sin_port = htons(8989);   // 服务器监听的端口, 字节序应该是网络字节序
        inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
        int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
        if(ret == -1)
        {
            perror("connect");
            exit(0);
        }
     
     
        int i = 0;
        // 通信
        while(1)
        {
            // 读数据
            char recvBuf[1024];
            // 写数据
            sprintf(recvBuf, "data: %d\n", i++);
            write(fd, recvBuf, strlen(recvBuf));
            // 如果客户端没有发送数据, 默认阻塞
            read(fd, recvBuf, sizeof(recvBuf));
            printf("recv buf: %s\n", recvBuf);
            sleep(1);
        }
     
        // 释放资源
        close(fd);
     
        return 0;
    }

上一篇:01网络编程第一天


下一篇:通信编程:Winsock socket 编程步骤与样例