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;
}

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章