代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

1. 概念

1.1、网络设计模式

1.1.1 B/S
  • Browser -> 浏览器 -> 客户端角色
    • html、css、js
    • 跨平台方便
    • 只能使用http/https协议
    • 缓存的数据比较少
  • Server -> 服务器
1.1.2 C/S
  • client -> 桌面应用程序(带窗口的)
    • 通信协议可以随意选择
    • 可以大量缓存或加载磁盘数据
    • 开发成本高, 不同平台需要开发对应的app
  • server -> 服务器

1.2、所谓的服务器开发?

  • 不是写服务器
  • 在已经存在的服务器上开发应用程序

1.3、IP和端口

# 只要电脑/终端能上网, 那么其必然有一个IP地址
1.linux中查看IP地址:ifconfig
  inet addr:192.168.237.131  
2.window中查看IP地址:ipconfig
# 在网络(局域网/外网)通信的时候通过IP地址定位一台主机
1.3.1 IP地址
  • IPV4 -> 常用
    • 实际是一个32位的整形数 -> 本质 -> 4字节 int a;
    • 我们看到的不是这个整形数, 点分十进制字符串 -> 192.168.247.135
      • 分成了4份, 每份1字节, 8bit -> char , 最大值为 255 -> 最大取值: 255.255.255.255
    • IP可以有多少个 232 - 1 个
  • IPV6 -> 正在推广, 还没有被广泛应用
    • 实际是一个128位的整形数

    • xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx , 分成了8份, 每份2字节 -> 每一部分以16进制的方式表示

      CDCD:910A:2222:5498:8475:1111:3900:2020

    • IP可以有多少个 2128 - 1 个

1.3.2 端口
  • 只有在网络通信的时候才需要端口
  • 一个IP定位一台主机, 这个电脑上运行了若干个应用程序
    • 发送的数据要给到某一个运行的程序, 需要精确定位
      • 通过端口可以定位到当前终端的某个进程
  • 在程序中如何表示一个端口:
    • unsigned short int -> 16位
      • 取值范围: 0 - 65535
      • 默认: 0 -1024 是给操作系统预留的
      • 主机上安装的应用程序, 通信也需要使用端口 (1025 - 5000)
      • 写程序的时候, 指定端口: 5000以上

1.4、OSI/ISO 网络分层模型

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

  • 四/七层模型

     底层               --------->            上层
    七层模型:
    1.物理层 2.数据链路层 3.网络层 4.运输层 5.会话层  6.表示层  7.应用层
    四层模型:
    1.网络接口层	         网际层IP  运输层	  应用层
    物理层.数据链路层      网络层	  运输层   会话层.表示层.应用层
    
  • 物理层:
    • 物理层负责最后将信息编码成电流脉冲或其它信号用于网上传输
  • 数据链路层:
    • 数据链路层通过物理网络链路供数据传输。
    • 规定了0和1的分包形式,确定了网络数据包的形式;
  • 网络层
    • 网络层负责在源和终点之间建立连接;
    • 此处需要确定计算机的位置,怎么确定?IPv4,IPv6
  • 运输层
    • 运输层向高层提供可靠的端到端的网络数据流服务。
    • 每一个端口号,该层就是端口与端口的通信
  • 会话层
    • 会话层建立、管理和终止表示层与实体之间的通信会话;
    • 建立一个连接(自动的手机信息、自动的网络寻址);
  • 表示层:
    • 对应用层数据编码和转化, 确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;
    • 可以理解为:解决不同系统之间的通信,eg:手机上的QQ和Windows上的QQ可以通信;
  • 应用层:
    • 规定数据的传输协议

在这里插入图片描述

2. 协议格式

协议: 网络通信的时候发送的数据的格式, 这个格式是双方约定好的, 这样就能正确的对数据进行解析

  • 一般我们使用的协议都是标准协议
    • ftp
    • ssh
    • http、telnet、OSPF
    • tcp、udp
    • ip、ICMP
    • ARP、RARP
  • 协议是可以自定义的
  • 协议的概念

    • TCP协议 -> 传输层协议

在这里插入图片描述

  • UDP协议 -> 传输层协议

在这里插入图片描述

  • IP协议 -> 网络层协议

在这里插入图片描述

  • 以太网帧协议

在这里插入图片描述

  • 数据的封装

在这里插入图片描述

3. socket编程

Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。

在这里插入图片描述

// socket是一套网络通信接口(API)
网络通信是怎么回事?
 1.网络通信分为两部分
  - 客户端	
  - 服务器端
 2.服务器端:
  - 提供服务, 不会主动连接客户端, 永远是被动接受连接的角色
  - 需要先于客户端启动
  - 需要绑定一个固定的端口
 3.客户端:
  - 主动连接服务器
  - 客户端需要知道服务器的IP和端口
宏观上看客户端和服务器通信 -> 主机和主机之间的通信
	如果主机对应的平台不同, 数据从主机A发送到主机B, 数据在解析过程中会不会有问题?
    -->有字节序的问题(数据的存储顺序) 
Intel的cup是x86架构
	数据的存储格式: 低位字节存储在内存的低地址位, 高位字节存储在内存的高地址位
如果不是x86架构(有一些)
	低位字节存储在内存的高地址位, 高位字节存储在内存的低地址位

3.1 字节序

在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。

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

目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian 和 Little-Endian,下面先从字节序说起。


在这里插入图片描述

《格列佛游记》中记载了两个征战的强国,你不会想到的是,他们打仗竟然和剥鸡蛋的姿势有关。

很多人认为,剥鸡蛋时应该打破鸡蛋较大的一端,这群人被称作“大端(Big endian)派”。可是当今皇帝的祖父小时候吃鸡蛋的时候碰巧将一个手指弄破了。所以,他的父亲(当时的皇帝)就下令剥鸡蛋必须打破鸡蛋较小的一端,违令者重罚,由此产生了“小端(Little endian)派”。

老百姓们对这项命令极其反感,由此引发了6次叛乱,其中一个皇帝送了命,另一个丢了王位。据估计,先后几次有11000人情愿受死也不肯去打破鸡蛋较小的一端!

  • 概念

    1、Little-Endian(小端) :主机字节序 host

    • 低低, 高高
    • 低位字节存储在内存的低地址位, 高位字节存储在内存的高地址位

    2、Big-Endian(大端) : 网络字节序 net

    • 低高, 高低
    • 低位字节存储在内存的高地址位, 高位字节存储在内存的低地址位
  • 字节序举例

    // 单字节没有字节序的问题
    // 使用16进制在内存中表示这两个数,即:
    // 一个字节: 8bit -> char -> 255(十进制) -> oxff(16进制)
    	- 0x12 34 56 78
       	大端方式存储:
    		内存低地址位        ---------------- > 内存的高地址位
    				0x12	0x34	0x56	0x78
        小端方式存储:
    		内存低地址位        ---------------- > 内存的高地址位
    				0x78	0x56	0x34	0x12      
    
    • 小端

在这里插入图片描述

  • 大端

在这里插入图片描述

  • 函数

    BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。

    #include <arpa/inet.h>
    
    // h : host -> 主机字节序
    // to:to
    // n : net -> 网络字节序
    // s : short -> 16位整形数
    // l : long -> 32位整形数
    
    // 主机字节序(小端) -> 网络字节序(大端)
    uint16_t htons(uint16_t hostshort);		// 对端口进行转换!!!!!!!!!
    
    // 网络字节序(大端) -> 主机字节序(小端)
    uint16_t ntohs(uint16_t netshort);		// 对端口进行转换!!!!!!!!!
    
    // 主机字节序(小端) -> 网络字节序(大端)
    uint32_t htonl(uint32_t hostlong);		// 对IP进行转换, 不常用
    
    // 网络字节序(大端) -> 主机字节序(小端)
    uint32_t ntohl(uint32_t netlong);		// 对IP进行转换, 不常用
    

3.2 IP地址转换

#include <arpa/inet.h>

// 主机字节序(小端) -> 网络字节序(大端)
// 点分十进制字符串(对应本地一个小端的整数) -> 大端的整数
int inet_pton(int af, const char *src, void *dst);
参数:
- af: 地址族协议
	- AF_INET: 转换的是ipv4的ip地址
	- AF_INET6: 转换的是ipv6的ip地址
- src: 字符串类型的IP地址
- dst: 转换之后得到的整数, 存储在dst指针指向的内存中 -> 传出参数
返回值:
	- 成功: 0
     - 失败: -1

// 网络字节序(大端) -> 主机字节序(小端)
// 大端的整数(网络字节序) -> 点分十进制字符串(对应本地一个小端的整数)
//                        192.168.111.222
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
- af: 地址族协议
	- AF_INET: 转换的是ipv4的ip地址
	- AF_INET6: 转换的是ipv6的ip地址
- src: 传入参数, 对应是要转换的大端的整形数IP
- dst: 存储转换之后的字符串类型的IP地址, 该指针要指向一块有效内存
- size: 修饰dst指向的内存大小(17字节正好)

3.3 sockaddr数据结构

在这里插入图片描述

// 这个结构体存储端口和IP地址的
// 客户端 -> 服务器(IP, Port)
// 1、struct sockaddr
// 往里面写入数据比较费劲
// 所以我们使用struct sockaddr_in代替truct sockaddr
struct sockaddr 
{
    sa_family_t sa_family;	// 地址族协议, ipv4, ipv6
    char        sa_data[14];
}

//2、struct sockaddr_in
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))
struct in_addr
{
    in_addr_t s_addr;
};  
struct sockaddr_in
{
    sa_family_t sin_family;  /* 地址族协议 */
    in_port_t sin_port;      /* 端口号 */
    struct in_addr sin_addr; /* 32位的整型IP地址  */
    /* Pad to size of `struct sockaddr'. */
    unsigned char sin_zero[sizeof (struct sockaddr) 
                           - __SOCKADDR_COMMON_SIZE 
                           - sizeof (in_port_t) 
                           - sizeof (struct in_addr)];
                           //最后一个参数忽略
};  

3.4 套接字函数

#include <sys/types.h>  /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>	// 包含这个头文件, 上边的两个就可以不写了

// socket()//创建用于监听的文件描述符/套接字
// 创建一个套接字, 目的:得到一个文件描述, 对内核内存进行操作
int socket(int domain, int type, int protocol);
参数:
    - domain: 地址族协议
        AF_INET: 使用的ip地址是ipv4
        AF_INET6: 使用的ip地址是ipv6
    - type: 指定使用的传输层协议
        - SOCK_STREAM: 流式协议
        - SOCK_DGRAM: 报式协议(报文)
    - protocol: 默认写0
        - 如果第二个参数type == SOCK_STREAM, 0代表默认流式协议就是tcp
        - 如果第二个参数type == SOCK_DGRAM, 0代表默认报式协议就是udp
返回值:
    成功: 文件描述符--> 用于监听 !!!
    失败: -1
// int lfd = socket(AF_INET,SOCK_STREAM,0);


// bind()//将:用于监听的套接字和本地IP端口进行绑定
// 将:一个文件描述符和本地的IP端口进行绑定
struct sockaddr_in addr;
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
    - sockfd: 通过socket函数调用得到的文件描述符
    - addr: 结构体, 其中可以存储IP和端口信息, 以及地址族协议
        - IP和端口是大端类型(网络字节序), 因此初始化的时候需要转换
    - addrlen: addr结构体对应的内存大小
返回值: 
    成功: 0
    失败: -1

// listen()//设置监听(给绑定成功的文件描述符设置监听)
// 监听有没有客户端连接服务器
int listen(int sockfd, int backlog);	
参数:
    - sockfd: 绑定成功的文件描述符
    - backlog: 同时监听的最大连接数, 最大值是128
      默认值存储在// /proc/sys/net/core/somaxconn
      使用cat 查看
返回值:
    成功: 0
    失败: -1

// accept()//等待并接受客户端请求-建立连接-得到用于通信的文件描述符
// 1.默认调用后阻塞线程
// 2.当有客户端连接的时候, 该函数解除阻塞, 和客户端建立连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
    - sockfd: 用于监听的套接字
    - addr: 传出参数, 里边有连接成功的客户端的IP和端口
        - 得到的IP和端口是大端, 如果想使用需要转换(小端)
    - addrlen: 传入传出参数
        - 传入: addr结构体占用的内存大小
        - 传出: addr结构体占用的内存大小
返回值:
    成功: 返回一个文件描述符, 用于通信!!!
    失败: -1
            
// connect()//客户端使用该函数连接服务器
// 阻塞函数, 连接成功之后, 解除阻塞
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
    - sockfd: 通信的文件描述符, 通过socket()得到
    - addr: 客户端要连接的服务器的IP和端口信息
        - IP和端口是大端类型(网络字节序), 因此初始化的时候需要转换
    - addrlen: addr结构体占的内存大小
返回值:
    成功:  0
    失败: -1
            
// 套接字通信过程中: 读写是默认阻塞的,可以修改文件描述符的属性变为非阻塞

// write()/send()//写数据
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags); 
// 默认: flag = 0

// read()/recv()//读数据
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);	
// 默认: flag = 0

在这里插入图片描述

4. TCP通信流程

TCP: 传输层通信协议

面向连接的, 安全的流式传输协议

  • 连接过程: 需要三次握手
  • 断开连接: 需要四次挥手
  • 使用tcp进行数据传输, 不会丢失数据
    • 有数据校验机制, 如果数据丢失了, 会进行数据重传
  • 流式:
    • 数据的接收和发送端对数据的操作的量可以是不对等的

在这里插入图片描述

服务器端-通信流程:

// 有两种文件描述符: 监听(1个), 通信(N个, 服务器和多少客户端成功建立连接, 就有多少个)

// 1.通过文件描述符进行网络IO操作, 数据存储到内核的内存缓冲区中,
//   网络通信和管道一样对应的也是内存中的一块内存
// 2.通过得到的文件描述符就可以操作内核中的内存数据了
1. 创建套接字, 得到一个文件描述符
    int fd = socket();
2. 将第一步得到的文件描述符和本地的IP端口进行绑定
    bind();
3. 设置监听(绑定成功的文件描述符) -> 成功之后可以接收客户端连接
    listen();
4. 如果有客户端连接服务器, 服务器接受请求并建立连接, 得到一个文件描述符
    int fd1 = accept();
5. 通信
    - 接收数据:read(); / recv();
    - 发送数据:write(); / send();
6. 通信完成, 关闭文件描述符
    close();
// 01server.c

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

int main()
{
    //1.创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        exit(0);
    }
    //2.绑定本地IP和端口
    struct sockaddr_in addr;
    addr.sin_port = htons(9000); //空闲端口即可
    inet_pton(AF_INET, "192.168.184.132", &addr.sin_addr.s_addr);
    addr.sin_family = AF_INET;
    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("bind");
        exit(0);
    }
    //3.设置监听
    ret = listen(fd, 128);
    if (ret == -1)
    {
        perror("listen");
        exit(0);
    }
    //4.阻塞等待客户端连接,并且接受建立连接
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    printf("等待客户端连接...\n");
    int cfd = accept(fd, (struct sockaddr *)&cliaddr, &len);
    if (cfd == -1)
    {
        perror("accept");
        exit(0);
    }

    char buf[24];
    printf("client IP:%s,port:%d\n",
           inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, buf, sizeof(buf)),
           ntohs(cliaddr.sin_port));

    //5.通信
    while (1)
    {
        //接受数据
        char buff[24];
        int len = read(cfd, buff, sizeof(buff));
        if (len > 0)
        {
            //接收到了对方的数据
            printf("recv buf:%s\n", buff);
        }
        else if (len == 0)
        {
            printf("client disconnect...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }

        //发送数据
        char *p = "你好,客户端...\n";
        write(cfd, p, strlen(p) + 1);
    }

    //6.关闭
    close(cfd);
    close(fd);
    return 0;
}

客户端-通信流程:

// 文件描述符1类: 通信的

1. 创建套接字
    int fd = socket();
    // 客户端的端口不需要调用函数进程绑定, 自动就绑定了, 
    // 如果非得自己绑定也可以: bind
2. 连接服务器: 在客户端需要知道服务器绑定的IP和端口是多少
    connect();
3. 通信
    - 接收数据:read(); / recv();
    - 发送数据:write(); / send();
4. 断开连接
    close():
// 01client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.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_port = htons(9000); //空闲端口即可
    inet_pton(AF_INET, "192.168.184.132", &addr.sin_addr.s_addr);
    addr.sin_family = AF_INET;
    int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret == -1)
    {
        perror("connect");
        exit(0);
    }

    //3.通信
    while (1)
    {

        //发送数据
        char *p = "你好,服务器...\n";
        write(fd, p, strlen(p) + 1);

        //接受数据
        char buff[24];
        int len = read(fd, buff, sizeof(buff));
        if (len > 0)
        {
            //接收到了对方的数据
            printf("recv buf:%s\n", buff);
            sleep(1);
        }
        else if (len == 0)
        {
            printf("server disconnect...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    //6.关闭
    close(fd);
    return 0;
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐