Linux网络编程(二):Socket 编程

2023-12-20 19:25:16

参考引用

1. 套接字概念

  • Socket 本身有 “插座” 的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型

    • 本质为内核借助缓冲区形成的伪文件
  • 既然是文件,那么可以使用文件描述符引用套接字

    • 与管道类似,Linux 系统将其封装成文件是为了统一接口,使得读写套接字和读写文件的操作一致
    • 区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递
  • 在 TCP/IP 协议中,“IP 地址 + TCP 或 UDP 端口号” 唯一标识网络通信中的一个进程

    • “IP 地址 + 端口号” 就对应一个 socket
    • 欲建立连接的两个进程各自有一个 socket 来标识,那么这两个 socket 组成的 socket pair 就唯一标识一个连接,因此可以用 socket 来描述网络连接的一对一关系
  • 套接字通信原理如下图所示

在这里插入图片描述

  • 在网络通信中,套接字一定是成对出现的
    • 一个文件描述符指向一个套接字 (该套接字内部由内核借助两个缓冲区实现)

在这里插入图片描述

2. 预备知识

2.1 网络字节序

  • 内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?

    • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节,而发送主机可能是小端字节序,也可能是大端字节序。为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换(PC 机采用小端法,网络采用大端法)

    • 如果主机是小端字节序,这些函数将参数做相应的大小端转换后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
    #include <arpa/inet.h>
    
    // h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数
    uint32_t htonl(uint32_t hostlong);    // 本地转网络(IP)
    uint16_t htons(uint16_t hostshort);   // 本地转网络(port) 
    uint32_t ntohl(uint32_t netlong);     // 网络转本地(IP)
    uint16_t ntohs(uint16_t netshort);    // 网络转本地(port)
    

2.2 IP 地址转换函数

#include <arpa/inet.h>

// 本地字节序 (string 形式的 IP)---> 网络字节序(二进制形式的 IP)
    // af:AF_INET(ipv4)、AF_INET6(ipv6)
    // src:传入参数,本地字节序 IP 地址(点分十进制)
    // dst:传出参数,转换后的网络字节序的 IP 地址
    // 返回值:成功 1;异常 0,说明 src 指向的不是一个有效的 IP 地址;失败 -1
int inet_pton(int af, const char *src, void *dst);

// 网络字节序(二进制形式的 IP)---> 本地字节序 (string 形式的 IP)
    // af:AF_INET(ipv4)、AF_INET6(ipv6)
    // src:传入参数,网络字节序 IP 地址
    // dst:传出参数,转换后的本地字节序(string IP)
    // size:dst 的大小
    // 返回值:成功,返回 dst;失败,返回 NULL
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的 in6_addr

2.3 sockaddr 数据结构

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

在这里插入图片描述

// man 7 ip 命令查看下列定义
// strcut sockaddr 已过时,现在统一使用 struct sockaddr_in
struct sockaddr_in {
    __kernel_sa_family_t sin_family;  /* Address family 地址结构类型 */  	
    __be16 sin_port;                  /* Port number 端口号 */		
    struct in_addr sin_addr;          /* Internet address IP 地址 */	
    
        /* Pad to size of `struct sockaddr'. */
    unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
    sizeof(unsigned short int) - sizeof(struct in_addr)];
};
  • IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位端口号和 32 位 IP 地址,IPv6 地址用 sockaddr_in6 结构体表示,包括 16 位端口号、128 位 IP 地址
    struct sockaddr_in addr;
    addr.sin_family = AF_INET/AF_INET6/AF_UNIX;  // AF_UNIX 用于本地套接字
    addr.sin_port = htons(9527);
    
    int dst;
    inet_pton(AF_INET, "192.157.22.45", (void*)&dst);
    addr.sin_addr.s_addr = dst;
    
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 取出系统中有效的任意 IP 地址,二进制类型
    
    bind(fd, (struct sockaddr *)&addr, size);
    

3. 网络套接字函数

3.1 socket 模型创建流程图

  • 1 个客户端和 1 个服务器端进行通信:一共有 3 个套接字(一对用于通信的套接字,服务器单独还有一个用于监听的套接字)
    在这里插入图片描述

3.2 socket 函数

// 创建一个套接字
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • domain
    • AF_INET 这是大多数用来产生 socket 的协议,使用 TCP 或 UDP 来传输,用 IPv4 的地址
    • AF_INET6 与上面类似,不过是用 IPv6 的地址
  • type
    • SOCK_STREAM(流式) 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的 socket 类型,这个 socket 使用 TCP 来进行传输
    • SOCK_DGRAM(报式) 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用 UDP 来进行它的连接
  • protocol
    • 传 0 表示使用默认协议
  • 返回值
    • 成功:返回新套接字(socket)对应的文件描述符
    • 失败:返回 -1,设置 errno

socket() 打开一个网络通讯端口,如果成功的话,就像 open() 一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write 在网络上收发数据,如果 socket() 调用出错则返回 -1

3.3 bind 函数

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址(IP)和端口号(port)到套接字

    #include <sys/types.h>
    #include <sys/socket.h>
    
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  • sockfd

    • 指定待绑定的 socket(用于监听的套接字) 文件描述符
  • addr

    • 构造出 IP 地址加端口号
    • 类型是 struct sockaddr *
    /*
        struct sockaddr_in {
            sa_family_t    sin_family; // address family: AF_INET
            in_port_t      sin_port;   // port in network byte order
            struct in_addr sin_addr;   // internet address
        };
        // internet address
        struct in_addr {
            uint32_t       s_addr;     // address in network byte order
        };
    
    */
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    // 因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听
    // 直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  // INADDR_ANY 宏:表示取出系统中有效的任意 IP 地址,二进制类
    (struct sockaddr *)&addr // addr 类型为 struct sockaddr*,故此处需要强制转换
    
  • addrlen

    • sizeof(addr) 结构体长度
  • 返回值

    • 成功:返回 0
    • 失败:返回 -1, 设置 errno

3.4 listen 函数

  • listen 函数用于设置同时与服务器建立连接的(监听)上限数 (同时进行 3 次握手的客户端数量)

    #include <sys/types.h>
    #include <sys/socket.h>
    
    int listen(int sockfd, int backlog);
    
  • sockfd

    • socket 对应的文件描述符
  • backlog

    • 排队建立 3 次握手队列和刚刚建立 3 次握手队列的链接数和
    • 最大值 128
  • 典型的服务器程序可以同时服务于多个客户端

    • 当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接
    • 如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态
    • listen() 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接等待状态,如果接收到更多的连接请求就忽略
    • listen() 成功返回 0,失败返回 -1

3.5 accept 函数

  • accept 函数用于阻塞(监听)等待客户端建立连接
    • 三方握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
    #include <sys/types.h>
    #include <sys/socket.h>
    
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
  • sockdf
    • socket 文件描述符,socket 函数返回值
  • addr
    • 传出参数,返回成功与服务器建立连接的那个客户端的地址结构(含 IP 地址和端口号)
    • 用于和客户端建立连接的套接字
  • addrlen
    • 传入传出参数
      • 传入:sizeof(addr) 大小
      • 传出:客户端 addr 实际大小
  • 返回值
    • 成功:返回一个新的用于和客户端通信的 socket 对应的文件描述符
    • 失败:返回 -1,设置 errno

3.6 connect 函数

  • connect 函数作用:使用现有的 socket 与服务器建立连接
    • connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址
    #include <sys/types.h>
    #include <sys/socket.h>
    
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  • sockdf
    • socket 文件描述符
  • addr
    • 传入参数,指定服务器端地址结构(含 IP 地址和端口号)
  • addrlen
    • 传入参数,传入 sizeof(addr) 服务器端地址结构的长度
  • 返回值
    • 成功:返回 0
    • 失败:返回 -1,设置 errno

如果不使用 bind 绑定客户端地址结构,则系统默认采用 “隐式绑定”

4. C/S 模型的 TCP 通信实现

4.1 实现分析

在这里插入图片描述

  • 服务器调用 socket()、bind()、listen() 完成初始化后,调用 accept() 阻塞等待,处于监听端口的状态,客户端调用 socket() 初始化后,调用 connect() 发出 SYN 段并阻塞等待服务器应答,服务器应答一个 SYN-ACK 段,客户端收到后从 connect() 返回,同时应答一个 ACK 段,服务器收到后从 accept() 返回

4.2 server 实现

  • 从客户端读字符,然后将每个字符转换为大写并回送给客户端
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 9527

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

int main(int argc, char *argv[]) {
    int lfd = 0, cfd = 0;
    int ret, i;
    char buf[BUFSIZ], client_IP[1024];

    struct sockaddr_in serv_addr, clit_addr;         // 定义 服务器地址结构 和 客户端地址结构
    socklen_t clit_addr_len;              // 客户端地址结构大小

    /*
        struct sockaddr_in {
            sa_family_t    sin_family; // address family: AF_INET
            in_port_t      sin_port;   // port in network byte order
            struct in_addr sin_addr;   // internet address
        };
        // internet address
        struct in_addr {
            uint32_t       s_addr;     // address in network byte order
        };

    */
    serv_addr.sin_family = AF_INET;                  // IPv4
    serv_addr.sin_port = htons(SERV_PORT);           // 转为网络字节序的 端口号
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);   // 获取本机任意有效 IP

    lfd = socket(AF_INET, SOCK_STREAM, 0);           // 创建一个 socket
    if (lfd == -1) {
        sys_err("socket error");
    }

    bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // 给服务器 socket 绑定地址结构(IP+port)

    listen(lfd, 128);                         // 设置监听上限

    clit_addr_len = sizeof(clit_addr);        // clit_addr_len为传入传出参数,此处获取客户端地址结构大小然后传入 accept
    cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); // 阻塞等待客户端连接请求
    if (cfd == -1)
        sys_err("accept error");

    printf("client ip:%s port:%d\n", 
            inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)), 
            ntohs(clit_addr.sin_port));       // 根据 accept 传出参数,获取客户端 ip 和 port

    while (1) {
        ret = read(cfd, buf, sizeof(buf));    // 读客户端数据
        write(STDOUT_FILENO, buf, ret);       // 写到屏幕查看

        for (i = 0; i < ret; i++)             // 小写 -- 大写
            buf[i] = toupper(buf[i]);

        write(cfd, buf, ret);                 // 将大写,写回给客户端
    }

    close(lfd);
    close(cfd);

    return 0;
}

4.3 client 实现

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

#define SERV_PORT 9527

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

int main(int argc, char *argv[]) {
    int cfd;
    int conter = 10;
    char buf[BUFSIZ];
    
    struct sockaddr_in serv_addr;          // 服务器地址结构

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);

    cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd == -1)
        sys_err("socket error");

    int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    if (ret != 0)
        sys_err("connect err");

    while (--conter) {
        write(cfd, "hello\n", 6);
        ret = read(cfd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, ret);
        sleep(1);
    }

    close(cfd);

    return 0;
}
# 测试例程
$ gcc server.c -o server
$ ./server
client ip : 127.0.0.1 port : 46914
hello
hello
hello
...
# 另开一个终端
$ gcc client.c -o client
$ ./client
HELLO
HELLO
HELLO
...

5. 出错处理封装函数

  • 系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。为使错误处理的代码不影响主程序的可读性,把与 socket 相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块 wrap.c

在这里插入图片描述

  • wrap.c
#include "wrap.h"

void perr_exit(const char *s) {
    perror(s);
    exit(-1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) {
    int n;

again:
    if ((n = accept(fd, sa, salenptr)) < 0) {
        if ((errno == ECONNABORTED) || (errno == EINTR))
            goto again;
        else
            perr_exit("accept error");
    }
    return n;
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen) {
    int n;
    
    if ((n = bind(fd, sa, salen)) < 0)
        perr_exit("bind error");
    
    return n;
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen) {
    int n;
    n = connect(fd, sa, salen);
    if (n < 0) {
        perr_exit("connect error");
    }
    
    return n;
}

int Listen(int fd, int backlog) {
    int n;
    
    if ((n = listen(fd, backlog)) < 0)
        perr_exit("listen error");
    
    return n;
}

int Socket(int family, int type, int protocol) {
    int n;
    
    if ((n = socket(family, type, protocol)) < 0)
        perr_exit("socket error");
    
    return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes) {
    ssize_t n;

again:
    if ( (n = read(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    
    return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes) {
    ssize_t n;

again:
    if ((n = write(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

int Close(int fd) {
    int n;
    if ((n = close(fd)) == -1)
        perr_exit("close error");
    
    return n;
}

/*参三: 应该读取的字节数*/                          //socket 4096  readn(cfd, buf, 4096)   nleft = 4096-1500
ssize_t Readn(int fd, void *vptr, size_t n) {
    size_t  nleft;              //usigned int 剩余未读取的字节数
    ssize_t nread;              //int 实际读到的字节数
    char   *ptr;
    
    ptr = vptr;
    nleft = n;                  //n 未读取字节数
    
    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else
                return -1;
        } else if (nread == 0)
            break;
    
        nleft -= nread;   //nleft = nleft - nread 
        ptr += nread;
    }
    return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n) {
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    
    ptr = vptr;
    nleft = n;
    while (nleft > 0) {
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

static ssize_t my_read(int fd, char *ptr) {
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];
    
    if (read_cnt <= 0) {
again:
        if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {   //"hello\n"
            if (errno == EINTR)
                goto again;
            return -1;
        } else if (read_cnt == 0)
            return 0;
    
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    
    return 1;
}

/*readline --- fgets*/    
//传出参数 vptr
ssize_t Readline(int fd, void *vptr, size_t maxlen) {
    ssize_t n, rc;
    char c, *ptr;
    ptr = vptr;
    
    for (n = 1; n < maxlen; n++) {
        if ((rc = my_read(fd, &c)) == 1) {   //ptr[] = hello\n
            *ptr++ = c;
            if (c == '\n')
                break;
        } else if (rc == 0) {
            *ptr = 0;
            return n-1;
        } else
            return -1;
    }
    *ptr = 0;
    
    return n;
}
  • wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_

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

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);

#endif

文章来源:https://blog.csdn.net/qq_42994487/article/details/135112974
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。