4. Linux网络编程

243 阅读10分钟

4.1网络结构模式

C/S模式

针对每个不同的客户端,拥有不同的客户端,大部分工作在客户机就完成,不是跨平台的,例如QQ LOL等等

优点

  • 能充分利用客户机性能,很多工作客户机处理后交给服务器,C/S结构客户端响应快
  • 满足客户的个性化需求
  • 有较强的事物处理能力,能完成较为复杂的需求

缺点

  • 对客户机有要求,非跨平台
  • 客户机需要安装对应的客户端软件,安装成本高,系统升级,每一客户端都需升级

B/S模式

通过浏览器访问服务器,比如4399小游戏等等

优点

B/S 架构最大的优点是总体拥有成本低、维护方便、 分布性强、开发简单,可以不用安装任何专门的软 件就能实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能 使用。

缺点

  1. 通信开销大、系统和数据的安全性较难保障;
  2. 个性特点明显降低,无法实现具有个性化的功能要求;
  3. 协议一般是固定的:http/https
  4. 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低

4.2,4,3 MAC地址,IP地址,端口

4.6,4.7网络通信过程

4.8 socket介绍

Linux中一切皆文件,socket是一个伪文件,socket是通信两端的抽象,抽象成点,里面封装了TCP/IP通信,用简单的api就能完成不同主机间的进程通信

image.png

4.9字节序

网络字节序 规定好为大端

主机字节序 可以为大端也可以为小端

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

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

int main() {

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

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

4.10 字节序转换函数

网络字节序 规定为大端

主机字节序 可以为大端也可以为小端

因此需要网络字节序主机字节序的相互转换函数

/*

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

    // 转换端口
    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 : %x\n", a);
    unsigned short b = htons(a);
    printf("b : %x\n", b);

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

    // htonl  转换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


    return 0;
}

4.11 socket地址

通用socket地址

struct sockaddr
  {
    sa_family_t sa_family;	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };
  
- sa_family是地址族类型的变量
- sa_data[14]用于存放socket地址值

sa_family

协议族和地址族宏值相等

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地域协议族
PF_INETAF_INETTCP/IPv4协议族
PF_INET6AF_INET6TCP/IPv6协议族

sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:

协议族地址值含义和长度
PF_UNIX文件的路径名,长度可达 108 字节
PF_INET16 bit 端口号和32 bit IPv4地址,共 6 字节
PF_INET616 bit 端口号,32 bit流标识,128 bit IPv6地址,32 bit范围ID,共 26 字节

由于无法满足 IPv6 ,Linux定义了一个新的 socket 地址结构体

struct sockaddr_storage
  {
    __SOCKADDR_COMMON (ss_);	/* Address family, etc.  */
    char __ss_padding[_SS_PADSIZE];
    __ss_aligntype __ss_align;	/* Force desired alignment.  */
  };

专用socket地址

  • 上述的两个通用socket地址结构体使用起来非常的麻烦,Linux为各个协议族提供了专门的socket地址结构体

  • 很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现 在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是
    sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型

  • 所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地 址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr

image.png

Unix本地专用

#include <sys/un.h>  
struct sockaddr_un  
{  
sa_family_t sin_family;  
char sun_path[108];  
};

TCP/IP 协议族专用

TCP/IP 协议族sockaddr_insockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4IPv6

#include <netinet/in.h>  
struct sockaddr_in  
{  
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */  
in_port_t sin_port; /* Port number. */  
struct in_addr sin_addr; /* Internet address. */  
/* Pad to size of `struct sockaddr'. */  
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;  
};  
struct sockaddr_in6  
{  
sa_family_t sin6_family;  
in_port_t sin6_port; /* Transport layer port # */  
uint32_t sin6_flowinfo; /* IPv6 flow information */  
struct in6_addr sin6_addr; /* IPv6 address */  
uint32_t sin6_scope_id; /* IPv6 scope-id */  
};  
typedef unsigned short uint16_t;  
typedef unsigned int uint32_t;  
typedef uint16_t in_port_t;  
typedef uint32_t in_addr_t;  
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

4.12 IP地址转换函数

该系列的函数是为了将利于人们阅读的格式和实际的格式之间的转换

    #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:转换后的结果保存在这个里面
        返回值:成功返回1,失败返回0并设置errno
        
    // 将网络字节序的整数,转换成点分十进制的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()
{
    char buf[] = "192.168.1.4"; 
    // 转化为网络字节序
    int num = 0;
    inet_pton(AF_INET, buf, (void*) &num);
    
    unsigned char* p = (unsigned char*)&num;
    printf("%d %d %d %d\n", *p, *(p + 1), *(p + 2), *(p + 3));
    
    char buf1[16] = {0};
    const char* temp = inet_ntop(AF_INET, &num, buf1, 16);
    printf("buf1 : %s\ntemp : %s\n%d\n", buf1, temp, temp == buf1);
    return 0;
}

4.13 TCP通信流程

image.png

服务器端

  1. 创建用于监听的套接字

    • 监听 : 监听有客户端的连接
    • 套接字 : 这个套接字就是一个文件描述符
  2. 将这个监听的文件描述符与本地的IPport号绑定(IP和端口号就是本地服务器的地址信息)

    • 客户端连接服务器时候使用的是这个IP和端口
  3. 设置监听,监听fd开始工作

  4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd

  5. 通信

    • 接受数据
    • 发送数据
  6. 通信结束断开连接

客户端

  1. 创建一个用于通信的套接字(fd)
    • 不用进行绑定,会随机分配,通信过程中双端能获得对方的IP和port
  2. 连接服务器,需要指定连接的服务器的 IP 和 端口
  3. 连接成功了,客户端可以直接和服务器通信
    • 接收数据
    • 发送数据
  4. 通信结束,断开连接

4.14 socket函数

image.png
        #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  
           
        - 返回值:  
          - 成功:返回文件描述符,操作的就是内核缓冲区。  
          - 失败:-1  
          
        int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socke命名  

        - 功能:绑定,将fd 和本地的IP + 端口进行绑定  
        - 参数:  
        - sockfd : 通过socket函数得到的文件描述符  
        - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息  
        - addrlen : 第二个参数结构体占的内存大小  
        - 返回值
           - 成功返回 0
           - 失败返回 -1,设置errornumber
           
        int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn  
        - 功能:监听这个socket上的连接  
        - 参数:  
        - sockfd : 通过socket()函数得到的文件描述符  
        - backlog : 未连接的和已经连接的和的最大值,提示内核监听队列的最大长度,一般设置为5 
        
        
        int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);  
        - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接  
        - 参数:  
        - sockfd : 用于监听的文件描述符  
        - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)  
        - addrlen : 指定第二个参数的对应的内存大小  
        - 返回值:  
        - 成功 :用于通信的文件描述符  
        - -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.15 TCP通信实现(服务器端)

#include "stdio.h"
#include "unistd.h"
#include "arpa/inet.h"
#include "string.h"
#include "stdlib.h"

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

//   2.绑定本地的IP和PORT

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    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, 5);
    if (ret == -1) {
        perror("listen");
        exit(-1);
    }

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

//    输出客户端信息
    char client[16] = {0};
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, client, sizeof client);
    int port;
    port = (clientaddr.sin_port);
    printf("ip : %s \n port : %d\n", client, port);

//   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("clinet closed...");
            break;
        }

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

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

4.16 TCP通信(客户端)

//
// Created by robin on 22-11-3.
//
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "arpa/inet.h"

int main() {
    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "0.0.0.0", &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...");
            break;
        }

    }

    // 关闭连接
    close(fd);

    return 0;
}

完成回射

1fcadc6d6f4ab7dbaa961012061a077.jpg

已经完成

完成从键盘录入后发送

129fb28bfe100dcd3ec7519c9c494d3.jpg

已经完成

TCP三次挥手

详解三次挥手