美文网首页
Linux 下的 Socket 编程详解 (一) 开始

Linux 下的 Socket 编程详解 (一) 开始

作者: 木头石头骨头 | 来源:发表于2019-08-17 17:01 被阅读0次

    简介

    在 Linux 环境下,Socket 套接字是计算机操作系统中用来编写 TCP/IP 通信的接口。它是一种 facade 模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口谋面。在 TCP/IP 协议族里,Socket 的位置如下如所示:

    OSI 模型和网际协议族中的映射

    Socket 起源于 Unix,而 Unix 基本哲学就是“一切皆文件”,都可以用“open->write/read->close”模式来操作。Socket 就是该模式的一个实现,Socket 即是一种特殊的文件,一些 Socket 函数就是对其进行的操作(读/写IO、打、关闭)

    • 服务器端的套接字操作的流程为:socket -> bind -> listen -> accept -> write/read ->close

    • 客户端的套接字操作的流程为:socket -> connect -> write/read -> close

    服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

    这些接口的实现都是内核来完成,我们的用户程序通过系统调用,使用内核接口,完成套接字的创建。

    参考文档

    一个最简单的例子

    服务器端

    #include <stdlib.h>
    #include <string.h>
    #include <errno.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <unistd.h>
    #include <iostream>
    
    #define PORT 12345
    
    
    void Server(){
    
        int socket_fd, connect_fd;  // 套接字描述符
        struct sockaddr_in servaddr;
        char buff[4096];
    
        // 创建 socket
        if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
            std::cout << "套接字创建失败, 错误: " << strerror(errno) << errno << std::endl;
            exit(0);
        }
    
        // 初始化
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP 地址设置成 INADDR_ANY, 让系统自动获取本机的 IP 地址
        servaddr.sin_port = htons(PORT); // 设置端口
    
        // 将本地地址 bind 到 socket 上
        if(bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
            std::cout << "绑定套接字失败, 错误: " << strerror(errno) << errno << std::endl;
            exit(0);
        }
    
        // 开始 listen 客户端连接
        if(listen(socket_fd, 10) == -1){
            std::cout << "开启监听失败, 错误: " << strerror(errno) << errno << std::endl;
            exit(0);
        }
    
        std::cout << "Waiting ......" << std::endl;
    
        // 阻塞直到有客户端连接, accept
        if((connect_fd = accept(socket_fd, (struct sockaddr*) nullptr, nullptr)) == -1){
            std::cout << "接受连接失败, 错误: " << strerror(errno) << errno << std::endl;
            exit(0);
        }
    
    
        // 接受客户端发送来的数据
        int n = recv(connect_fd, buff, sizeof(buff), 0);
        buff[n] = '\0';
        std::cout << buff << std::endl;
    
        // 向客户端发回数据
        char sendBack[] = "I Received";
        send(connect_fd, sendBack, strlen(sendBack), 0);
    
        close(connect_fd);
        close(socket_fd);
    }
    

    客户端

    #include <stdlib.h>
    #include <string.h>
    #include <errno.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <iostream>
    
    #define PORT 12345
    
    void Client() {
    
        int socket_fd;
        char buff[] = "hello";
        struct sockaddr_in servaddr;
    
    
        if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
            std::cout << "套接字创建失败, 错误: " << strerror(errno) << errno << std::endl;
            exit(0);
        }
    
        memset(&servaddr, 0, sizeof(servaddr));
    
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(PORT);
        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 将点分十进制IP地址转换成整数
    
        // 创建连接
        if(connect(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
            std::cout << "连接创建失败, 错误: " << strerror(errno) << errno << std::endl;
            exit(0);
        }
    
    
        // 发送数据到服务器端
        if(send(socket_fd, buff, strlen(buff), 0) < 0){
            std::cout << "数据发送失败, 错误: " << strerror(errno) << errno << std::endl;
            exit(0);
        }
    
        // 接受服务器发来的数据
        char sendBack[2048];
        int n = recv(socket_fd, sendBack, sizeof(sendBack), 0);
        sendBack[n] = '\0';
        std::cout << sendBack << std::endl;
    
        close(socket_fd);
    
    }
    

    代码解释

    创建套接字(socket)

    首先,不管在客户端还是在服务器端,都需要调用 socket 函数,创建套接字。调用函数后,会返回一个整形的数字,这个数字被称为文件描述符(因为套接字也是一个特殊的文件)。在后面的套接字操作中,会使用到这个描述符。

    对于文件描述符和文件指针,可以去看另外的文章详解

    int socket(int protofamily, int type, int protocol) 函数接受三个参数。

    1. protofamily :协议族,常用的协议族有:AF_INET(IPv4)AF_INET6(IPv6)AF_LOCAL(Unix 域 Socket)。协议族决定了 套接字的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 + PORT 的组合
    2. type :指定套接字的类型
      • SOCK_STREAM
      • SOCK_DGRAM
      • SOCK_RAW
      • SOCK_PACKET
      • SOCK_SEQPACKET
    3. protocol :指定运输层的协议,包括IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC。分别对应 TCP,UDP,SCTP,TIPC

    绑定 IP 地址(bind)

    使用 bind 函数绑定 IP 地址,

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

    • sockfd :套接字描述符,通过 socket 创建,唯一标识一个 socket。bind 函数就是将给这个描述符绑定一个名字
    • addr :一个 const struct sockaddr* 指针,指向要绑定给 sockfd 的协议地址。其中传入这个参数的结构体如下:
    // ipv4
    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 */
    };
    
    // ipv6
    struct sockaddr_in6 { 
        sa_family_t     sin6_family;   /* AF_INET6 */ 
        in_port_t       sin6_port;     /* port number */ 
        uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
        struct in6_addr sin6_addr;     /* IPv6 address */ 
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
    };
    
    struct in6_addr { 
        unsigned char   s6_addr[16];   /* IPv6 address */ 
    };
    
    • addrlen :对应地址的长度

    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

    监听(listen)和连接(connect)

    绑定 ip 地址后,服务器端就开启接听,等待客户端的连接。客户端创建套接字后,就通过指定 ip 地址连接服务器。

    int listen(int sockfd, int backlog)

    • sockfd :套接字描述符
    • backlog :可以排队的最大连接个数

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

    与服务器端的 bind 函数参数一样

    应答(accept)

    当有客户端 connect 到服务器,这时 accept 函数就会有响应,接受客户端的请求,这样连接就建立好了。之后就可以开始网络 I/O 操作,类似于不同的文件的读写 I/O。accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

    • sockfd :套接字描述符
    • addr :这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
    • addrlen :上面 addr 结构的大小,同样也可置为 NULL

    读(read)写(write)

    建立连接后,就可以开始网络 I/O 的读写。网络 I/O 操作有下面几组:

    • read()/write()
    • recv()/send()
    • readv()/writev()
    • recvmsg()/sendmsg()
    • recvfrom()/sendto()

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    recv 函数从套接字描述符中读取内容,读取成功时,返回实际所读的字符数,如果小于 0 表示出错,send 函数同理。

    flag :默认为 0。其他取值如下:

    • MSG_DONTROUTE 绕过路由表查找,send 独占取值
    • MSG_DONTWAIT 仅本操作非阻塞,send 和 recv 均可设置
    • MSG_OOB 发送或接收带外数据,send 和 recv 均可设置
    • MSG_PEEK 窥看外来消息,recv 独占取值
    • MSG_WAITALL 等待所有数据,recv 独占取值

    关闭(close)

    在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    int close(int fd)

    相关文章

      网友评论

          本文标题:Linux 下的 Socket 编程详解 (一) 开始

          本文链接:https://www.haomeiwen.com/subject/setksctx.html