uc笔记10---网络通信,套接字(Socket),基于 TCP 协议的客户机/服务器模型

来源:互联网 发布:js数组去重复 编辑:程序博客网 时间:2024/05/06 21:20
1.    基本概念

                            ISO/OSI 七层网络协议模型
    ===================================================================
    -+-------------+-----------------+-    
    |    应用层        |    Application    |    
    -+-------------+-----------------+-    
    |    表示层        |    Presentation    |    
    -+-------------+-----------------+----------------
    |    会话层        |    Session        |    创建连接
    -+-------------+-----------------+-
    |    传输层        |    Transport        |
    -+-------------+-----------------+-
    |    网络层        |    Network        |
    -+-------------+-----------------+-    
    |    数据链路层    |    Data Link        |    分组、整合
    -+-------------+-----------------+-
    |    物理层        |    Physical        |
    -+-------------+-----------------+-----------------

                            TCP/IP 协议族
    ========================================================================
    1) TCP (Transmission Control Protocol, 传输控制协议) 面向连接的服务。
    2) UDP (User Datagram Protocol, 用户数据报文协议) 面向无连接的服务。
    3) IP (Internet Protocol, 互联网协议) 信息传递机制;TCP/UDP 低层的协议;

                    TCP/IP 协议与 ISO/OSI 模型的对比
    =======================================================================
           ISO/OSI       TCP/IP
        +--------------+-------------+----------------------------
        |    应用层        |                |                        |
        +--------------+                |                        |    高
        |    表示层        |    应用层        |    TELNET/FTP/HTTP    |    
        +--------------+                |                        |    层
        |    会话层        |                |                        |
        +--------------+-------------+----------------------------
        |    传输层        |    传输层        |    TCP/UDP            |
        +--------------+-------------+---------------------+
        |    网络层        |    互联网层    |    IP/路由            |    低
        +--------------+-------------+---------------------+
        |    数据链路层    |                |                        |    层
        +--------------+    网络接口层    |    驱动/设备            |
        |    物理层        |                |                        |
        +--------------+-------------+----------------------------

                消息包
        =====================
        +-----------------+
        | TELNET/FTP/HTTP |
        +-----------------+
        |     TCP/UDP     |
        +-----------------+
        |       IP        |
        +-----------------+
        |     ETHERNET    |
        +-----------------+

        从上至下,消息包逐层递增,
        从下至上,消息包逐层递减。

    IP 地址
    ========================================
    1) IP 地址是 Internet 中唯一的地址标识
    A. 一个 IP 地址占 32 位,正在扩充至 128 位。
    B. 每个 Internet 包必须带 IP 地址。

    2) 点分十进制表示法
    0x01020304 -> 1.2.3.4,高数位在左,低数位在右。

    3) IP地址分级
    A 级:0 + 7 位网络地址 + 24 位本地地址
    B 级:10 + 14 位网络地址 + 16 位本地地址
    C 级:110 + 21 位网络地址 + 8 位本地地址
    D 级:1110 + 28 位多播 (Muticast) 地址

    4) 子网掩码
    IP 地址 & 子网掩码 = 网络地址
    
    IP 地址: 192.168.182.48
    子网掩码:255.255.255.0
    网络地址:192.168.182
    本地地址:48
    
    127.0.0.1: 回绕地址,表示本机,不依赖网络。

2.    套接字 (Socket)

    接口:
       PuTTY -> telnet \
     LeapFTP ->    ftp -> socket -> TCP/UDP -> IP -> 网卡驱动 -> 网卡硬件
          IE ->   http /     ^
                                |
    应用程序 -----------------+

    异构:
    Java @ UNIX -> socket <----> socket <- C/C++ @ Windows

    模式:
    1) 点对点 (Peer-to-Peer, P2P):一对一的通信。
    2) 客户机/服务器 (Client/Server, C/S):一对多的通信。

    绑定:
    先要有一个套接字描述符,还要有物理通信载体,然后将二者绑定在一起。

    函数:
    1) 创建套接字
    #include <sys/socket.h>
    int socket (int domain, int type, int protocol);

    domain   - 域/地址族,取值:
                AF_UNIX/AF_LOCAL/AF_FILE: 本地通信 (进程间通信);
                AF_INET: 基于 TCP/IPv4 (32 位 IP 地址) 的网络通信;
                AF_INET6: 基于 TCP/IPv6 (128 位 IP 地址) 的网络通信;
                AF_PACKET: 基于底层包接口的网络通信。
    type     - 通信协议,取值:
                SOCK_STREAM: 数据流协议,即 TCP 协议;
                SOCK_DGRAM: 数据报协议,即 UDP 协议。
    protocol - 特别通信协议,一般不用,置 0 即可。
    成功返回套接字描述符,失败返回 -1。

    套接字描述符类似于文件描述符,UNIX 把网络当文件看待,
    发送数据即写文件,接收数据即读文件,一切皆文件。

    2) 准备通信地址
    A. 基本地址类型
    struct sockaddr {
        sa_family_t sa_family;    // 地址族(取值和上面 domain 一样)
        char        sa_data[14];    // 地址值
    };

    B. 本地地址类型
    #include <sys/un.h>            // un 指 unix
    struct sockaddr_un {
        sa_family_t sun_family;    // 地址族
        char        sun_path[];    // 套接字文件路径
    };

    C. 网络地址类型
    #include <netinet/in.h>
    struct sockaddr_in {
        // 地址族
        sa_family_t    sin_family;
        
        // 端口号:unsigned short,0-65535
        // 逻辑上表示一个参与通信的进程,使用时需要转成网络字节序(大端格式)
        // 0-1024 端口一般被系统占用;如:21: FTP;23: Telnet;80: WWW
        in_port_t      sin_port;
        // IP地址
        struct in_addr sin_addr;
    };
    struct in_addr {
        in_addr_t s_addr;
    };
    typedef uint32_t in_addr_t;

    IP 地址用于定位主机,端口号用于定位主机上的进程。

    3) 将套接字和通信地址绑定在一起
    #include <sys/socket.h>
    int bind (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
    sockfd:socked 描述符;
    addr:可以是 sockaddr_un,也可以是 sockaddr_in;
    addrlen:地址长度;
    成功返回 0,失败返回 -1。

    4) 建立连接
    #include <sys/socket.h>
    int connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
    成功返回 0,失败返回 -1。

    5) 用读写文件的方式通信:read/write
    6) 关闭套接字:close
    7) 字节序转换
    #include <arpa/inet.h>

    // 32 位无符号整数,主机字节序 -> 网络字节序
    uint32_t htonl (uint32_t hostlong);

    // 16 位无符号整数,主机字节序 -> 网络字节序
    uint16_t htons (uint16_t hostshort);

    // 32 位无符号整数,网络字节序 -> 主机字节序
    uint32_t ntohl (uint32_t netlong);

    // 16 位无符号整数,网络字节序 -> 主机字节序
    uint16_t ntohs (uint16_t netshort);

    主机字节序因处理器架构而异,有的采用小端字节序,有的采用大端字节序。
    网络字节序则固定采用大端字节序。

    8) IP 地址转换
    #include <arpa/inet.h>

    // 点分十进制字符串 -> 网络字节序32位无符号整数(不带检查)
    in_addr_t inet_addr (const char* cp);

    // 点分十进制字符串 -> 网络字节序32位无符号整数(自带检查)
    int inet_aton (const char* cp, struct in_addr* inp);

    // 网络字节序32位无符号整数 -> 点分十进制字符串
    char* inet_ntoa (struct in_addr in);

    编程:

    1) 本地通信

    服务器:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并绑定->接收数据->关闭套接字
    客户机:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并连接->发送数据->关闭套接字

    范例:locsvr.c
        #include <stdio.h>
        #include <sys/socket.h>
        #include <sys/un.h>
        // 本地通信需要路径;
        #define SOCK_FILE "/tmp/sock"
        int main (void) {
            printf ("服务器:创建本地数据报套接字...\n");
            // 第一步:创建本地套接字,返回套接字描述符;
            int sockfd = socket (AF_LOCAL, SOCK_DGRAM, 0);
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            printf ("服务器:准备地址并绑定...\n");
            // 第二步:准备地址并绑定套接字;
            struct sockaddr_un addr;                // 地址结构
            addr.sun_family = AF_LOCAL;            // 地址族
            strcpy (addr.sun_path, SOCK_FILE);    // 路径
            // 绑定
            if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                perror ("bind");
                return -1;
            }
            printf ("服务器:接收数据...\n");
            // 第三步:接收数据
            for (;;) {
                char buf[1024];
                // 一切皆文件思想:read 可以用于接受数据;
                ssize_t rb = read (sockfd, buf, sizeof (buf));
                if (rb == -1) {
                    perror ("read");
                    return -1;
                }
                // 约定:用两个感叹号关闭套接字(关闭服务器);
                if (! strcmp (buf, "!!"))
                    break;
                // 打印收到的任何数据;
                printf ("< %s\n", buf);
            }
            printf ("服务器:关闭套接字...\n");
            // 第四步:关闭套接字
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("服务器:删除套接字文件...\n");
            // 第五步:删除套接字文件(针对本地的才需要)
            if (unlink (SOCK_FILE) == -1) {
                perror ("unlink");
                return -1;
            }
            printf ("服务器:大功告成!\n");
            return 0;
        }
    
    范例:loccli.c
        #include <stdio.h>
        #include <sys/socket.h>
        #include <sys/un.h>
        #define SOCK_FILE "/tmp/sock"
        int main (void) {
            printf ("客户机:创建本地数据报套接字...\n");
            // 第一步:创建套接字
            int sockfd = socket (AF_LOCAL, SOCK_DGRAM, 0);
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            printf ("客户机:准备地址并连接...\n");
            // 第二步:准备连接;
            struct sockaddr_un addr;
            addr.sun_family = AF_LOCAL;
            strcpy (addr.sun_path, SOCK_FILE);
            // 建立连接;
            if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                perror ("connect");
                return -1;
            }
            printf ("客户机:发送数据...\n");
            // 第三步:发送数据;
            for (;;) {
                printf ("> ");
                char buf[1024];
                gets (buf);
                // 一个感叹号关闭客户端套接字;
                if (! strcmp (buf, "!"))
                    break;
                // 一切皆文件思想:write 可以用于发送;
                if (write (sockfd, buf, (strlen (buf) + 1) *
                    sizeof (buf[0])) == -1) {
                    perror ("write");
                    return -1;
                }
                // 输入两个感叹号,关闭服务器和客户端的套接字;
                if (! strcmp (buf, "!!"))
                    break;
            }
            // 第四步:关闭套接字;
            printf ("客户机:关闭套接字...\n");
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("客户机:大功告成!\n");
            return 0;
        }
        测试运行的时候需要开启两个窗口,分别运行客户端和服务器端;

    2) 网络通信

    服务器:创建套接字(AF_INET)->准备地址(sockaddr_in)并绑定->接收数据->关闭套接字
    客户机:创建套接字(AF_INET)->准备地址(sockaddr_in)并连接->发送数据->关闭套接字

    范例:netsvr.c
        #include <stdio.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        int main (int argc, char* argv[]) {
            if (argc < 2) {
                fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
                return -1;
            }
            printf ("服务器:创建网络数据报套接字...\n");
            int sockfd = socket (AF_INET, SOCK_DGRAM, 0);        // UDP
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            printf ("服务器:准备地址并绑定...\n");
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons (atoi (argv[1]));    // 端口号,先转为整数,在转为网络字节数;
            addr.sin_addr.s_addr = INADDR_ANY;            // IP 地址,计算机自动分配;
            // addr.sin_addr.s_addr = inet_addr ("127.0.0.1");
            if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                perror ("bind");
                return -1;
            }
            printf ("服务器:接收数据...\n");
            for (;;) {
                char buf[1024];
                ssize_t rb = read (sockfd, buf, sizeof (buf));
                if (rb == -1) {
                    perror ("read");
                    return -1;
                }
                if (! strcmp (buf, "!!"))
                    break;
                printf ("< %s\n", buf);
            }
            printf ("服务器:关闭套接字...\n");
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("服务器:大功告成!\n");
            return 0;
        }    
    
    范例:netcli.c
        #include <stdio.h>
        #include <string.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        int main (int argc, char* argv[]) {
            if (argc < 3) {
                fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
                return -1;
            }
            printf ("客户机:创建网络数据报套接字...\n");
            int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            printf ("客户机:准备地址并连接...\n");
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons (atoi (argv[2]));
            addr.sin_addr.s_addr = inet_addr (argv[1]);    // 手动获得 IP 地址;
            if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                perror ("connect");
                return -1;
            }
            printf ("客户机:发送数据...\n");
            for (;;) {
                printf ("> ");
                char buf[1024];
                gets (buf);
                if (! strcmp (buf, "!"))
                    break;
                if (write (sockfd, buf, (strlen (buf) + 1) *
                    sizeof (buf[0])) == -1) {
                    perror ("write");
                    return -1;
                }
                if (! strcmp (buf, "!!"))
                    break;
            }
            printf ("客户机:关闭套接字...\n");
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("客户机:大功告成!\n");
            return 0;
        }
        运行测试:
        首先通过下面命令查看服务器 ip 地址;
        #sbin/ifconfig
        然后在客户端输入 ip 地址和端口号,即可建立连接;

3.    基于 TCP 协议的客户机/服务器模型

    基本特征:
    1) 面向连接。
    2) 可靠,保证数据的完整性和有序性。

        ABCDEF
        A ->         -
        B ->         |
        C ->         +- 时间窗口
        D ->         |
        E -> <- A OK -
        F -> <- B OK
             <- C OK
             <- D OK
             <- E OK
             <- F OK

    每个发送都有应答,若在时间窗口内没有收到 A 的应答,则从 A 开始重发。

    编程模型:
    -------+-------------------------+----------------------------+------
        步骤|            服务器            |            客户机                |    步骤
    -------+--------------+----------+-------------+--------------+------
        1    |    创建套接字    |    socket    |    socket        |    创建套接字    |    1
        2    |    准备地址    |    . . .     |    . . .            |    准备地址    |    2
        3    |    绑定套接字    |    bind     |                |    ----        |    
        4    |    监听套接字    |    listen    |                |    ----        |    
        5    |    接受连接    |    accept    |    connect    |    建立链接    |    3
        6    |    接收请求    |    recv    |    send        |    发送请求    |    4
        7    |    发送响应    |    send    |    recv        |    接收响应    |    5
        8    |    关闭套接字    |    close    |    close        |    关闭套接字    |    6
    -------+-------------+-----------+-------------+--------------+------

    常用函数:
    #include <sys/socket.h>
    int listen (int sockfd, int backlog);
    1)将 sockfd 参数所标识的套接字标记为被动模式,使之可用于接受连接请求。
    2)backlog 参数表示未决连接请求队列的最大长度,即最多允许同时有多少个未决连接请求存在。
    若服务器的未决连接数已达上限,则客户机端的 connect() 函数返回 -1;且 errno 为 ECONNREFUSED;
    3)成功返回 0,失败返回 -1。

    int accept (int sockfd, struct sockaddr* addr, socklen_t* addrlen);
    1)从 sockfd 参数所标识套接字的未决连接请求队列中,
    提取第一个连接请求,同时创建一个新的套接字,
    用于在该连接中通信,返回该套接字的描述符。
    2)addr 和 addrlen 参数用于输出连接请求发起者的地址信息。
    3)成功返回通信套接字描述符,失败返回 -1。

    ssize_t recv (int sockfd, void* buf, size_t len, int flags);
    1)通过 sockfd 参数所标识的套接字,期望接收 len 个字节到 buf 所指向的缓冲区中。
    2)成功返回实际接收到的字节数,失败返回 -1。

    ssize_t send (int sockfd, const void* buf, size_t len, int flags);
    1)通过 sockfd 参数所标识的套接字,从 buf 所指向的缓冲区中发送 len 个字节。
    2)成功返回实际被发送的字节数,失败返回 -1。

    范例:tcpsvr.c
        #include <stdio.h>
        #include <stdlib.h>
        #include <signal.h>
        #include <wait.h>
        #include <errno.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        #include <arpa/inet.h>
        // 处理服务器创建的子进程僵尸;
        void sigchld (int signum) {
            for (;;) {
                pid_t pid = waitpid (-1, 0, WNOHANG);    // waitpid 默认非阻塞模式;
                // waitpid 失败返回 -1;
                if (pid == -1) {
                    if (errno != ECHILD) {
                        perror ("waitpid");
                        exit (-1);
                    }
                    // 子进程全部回收完毕 waitpid 返回 ECHILD;
                    printf ("服务器:全部子进程都已退出。\n");
                    break;
                }
                // waitpid 正常情况下返回被回收子进程的 pid
                if (pid)
                    printf ("服务器:发现%u子进程退出了。\n", pid);
                else {
                    printf ("服务器:暂时没发现有子进程退出。\n");
                    break;
                }
            }
        }
        int main (int argc, char* argv[]) {
            if (argc < 2) {
                fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
                return -1;
            }
            // 回收子进程;
            if (signal (SIGCHLD, sigchld) == SIG_ERR) {
                perror ("signal");
                return -1;
            }
            // 第一步:创建流套接字(用于监听);
            printf ("服务器:创建网络数据流套接字...\n");
            int sockfd = socket (AF_INET, SOCK_STREAM, 0);    // TCP
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            // 第二步:准备地址并绑定;
            printf ("服务器:准备地址并绑定...\n");
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons (atoi (argv[1]));
            addr.sin_addr.s_addr = INADDR_ANY;
            if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                perror ("bind");
                return -1;
            }
            // 第三步:监听套接字;
            printf ("服务器:监听套接字...\n");
            if (listen (sockfd, 1024) == -1) {
                perror ("listen");
                return -1;
            }
            // 第四步:等待连接请求;
            for (;;) {
                printf ("服务器:等待连接请求...\n");
                // 定义地址结构,给 accept 参数调用;
                struct sockaddr_in addrcli = {};
                socklen_t addrlen = sizeof (addrcli);
                // accept 返回连接套接字;后两个参数是地址,而非数值;
                int connfd = accept (sockfd, (struct sockaddr*)&addrcli, &addrlen);    // 父进程阻塞点;
                if (connfd == -1) {
                    perror ("accept");
                    return -1;
                }
                // 第五步:连接成功,同时创建子进程;
                printf ("服务器:接受来自%s:%u客户机的连接请求。" "创建子进程为其提供服务...\n",
                    inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
                pid_t pid = fork ();
                if (pid == -1) {
                    perror ("fork");
                    return -1;
                }
                if (pid == 0) {
                    printf ("%u子进程:为%s:%u客户机提供服务...\n", getpid (),
                        inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
                    // 子进程不需要监听套接字,所以先关闭
                    if (close (sockfd) == -1) {
                        perror ("close");
                        return -1;
                    }
                    for (;;) {
                        char buf[1024];
                        printf ("%u子进程:接收请求...\n", getpid ());
                        // 通过 recev 创建连接套接字;
                        ssize_t rb = recv (connfd, buf, sizeof (buf), 0);        // 子进程阻塞点
                        if (rb == -1) {
                            perror ("recv");
                            return -1;
                        }
                        if (rb == 0) {
                            printf ("%u子进程:客户机已关闭连接。\n", getpid ());
                            break;
                        }
                        printf ("%u子进程:发送响应...\n", getpid ());
                        if (send (connfd, buf, rb, 0) == -1) {
                            perror ("send");
                            return -1;
                        }
                    }
                    // 第六步:关闭子进程连接套接字,父进程可以继续监听其他套接字;
                    printf ("%u子进程:关闭连接套接字...\n", getpid ());
                    if (close (connfd) == -1) {
                        perror ("close");
                        return -1;
                    }
                    printf ("%u子进程:即将退出。\n", getpid ());
                    return 0;
                }
            }
            // 第七步:关闭服务器监听套接字;
            printf ("服务器:关闭监听套接字...\n");
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("服务器:大功告成!\n");
            return 0;
        }
        注意:我们一开始创建的 socket 在 listen 阶段,我们给他加了“耳朵”,
        那么这个 socket 只用于监听,不用于后期的数据传输;
        在 accept 阶段,会返回一个 socket,这个 socket 是用于传输用的;

    范例:tcpcli.c
        #include <stdio.h>
        #include <string.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        int main (int argc, char* argv[]) {
            if (argc < 3) {
                fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
                return -1;
            }
            // 第一步:创建流套接字;
            printf ("客户机:创建网络数据流套接字...\n");
            int sockfd = socket (AF_INET, SOCK_STREAM, 0);
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            // 第二步:准备地址,建立连接;
            printf ("客户机:准备地址并连接...\n");
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons (atoi (argv[2]));
            addr.sin_addr.s_addr = inet_addr (argv[1]);
            // 连接
            if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                perror ("connect");
                return -1;
            }
            // 第三步:发送请求,接收响应;
            printf ("客户机:发送请求并接收响应...\n");
            for (;;) {
                printf ("> ");
                char buf[1024];
                gets (buf);            // 阻塞点;
                // 叹号退出;
                if (! strcmp (buf, "!"))
                    break;
                // 发送请求;
                if (send (sockfd, buf, (strlen (buf) + 1) *
                    sizeof (buf[0]), 0) == -1) {    // 0 非阻塞方式
                    perror ("send");
                    return -1;
                }
                // 接收响应;
                ssize_t rb = recv (sockfd, buf, sizeof (buf), 0);
                if (rb == -1) {
                    perror ("recv");
                    return -1;
                }
                if (rb == 0) {
                    printf ("客户机:服务器已宕机!\n");
                    break;
                }
                printf ("< %s\n", buf);
            }
            // 第四步:关闭套接字;
            printf ("客户机:关闭套接字...\n");
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("客户机:大功告成!\n");
            return 0;
        }

4.    基于 UDP 协议的客户机/服务器模型

    基本特征    
    1) 无连接。
    2) 不可靠,不保证数据的完整性和有序性。

                A
                / \
               /   \
        ABC ->+-(B)-+-> CA
               \   /
                \ /
                C

    效率高速度快。

    编程模型
    -------+----------------------------+----------------------------+------
        步骤|            服务器                |            客户机                |    步骤
    -------+--------------+-------------+-------------+--------------+------
        1    |    创建套接字    |    socket        |    socket        |    创建套接字    |    1
        2    |    准备地址    |    . . .         |    . . .            |    准备地址    |    2
        3    |    绑定套接字    |    bind         |                |    ----        |    
        4    |    接收请求    |    recvfrom    |    sendto        |    发送请求    |    3
        5    |    发送响应    |    sendto        |    recvfrom    |    接收响应    |    4
        6    |    关闭套接字    |    close        |    close        |    关闭套接字    |    5
    -------+-------------+--------------+-------------+--------------+------

    常用函数

    #include <sys/socket.h>

    ssize_t recvfrom (int sockfd, void* buf, size_t len,
        int flags, struct sockaddr* src_addr, socklen_t* addrlen);
    1)通过 sockfd 参数所标识的套接字,期望接收 len 个字节到 buf 所指向的缓冲区中。
    2)若 src_addr 和 addrlen 参数不是空指针,则通过这两个参数输出源地址结构及其长度。
    注意在这种情况下,addrlen 参数的目标应被初始化为 src_addr 参数的目标数据结构的大小。
    3)flags 为 0 表示阻塞模式;
    4)成功返回实际接收到的字节数,失败返回 -1。

    ssize_t sendto (int sockfd, const void* buf, size_t len, int flags,
        const struct sockaddr* dest_addr, socklen_t addrlen);
    1)通过 sockfd 参数所标识的套接字,
    从 buf 所指向的缓冲区中发送 len 个字节。
    2)发送目的的地址结构及其长度,
    通过 dest_addr 和 addrlen 参数输入。
    3)成功返回实际被发送的字节数,失败返回 -1。

    范例:udpsvr.c
        #include <stdio.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        #include <arpa/inet.h>
        int main (int argc, char* argv[]) {
            if (argc < 2) {
                fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
                return -1;
            }
            // 第一步:创建数据报套接字;
            printf ("服务器:创建网络数据报套接字...\n");
            int sockfd = socket (AF_INET, SOCK_DGRAM, 0);    // UDP
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            // 第二步;准备地址,绑定;
            printf ("服务器:准备地址并绑定...\n");
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons (atoi (argv[1]));
            addr.sin_addr.s_addr = INADDR_ANY;
            if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                perror ("bind");
                return -1;
            }
            // 第三步:接收请求;发送响应;
            for (;;) {
                printf ("服务器:接收请求...\n");
                char buf[1024];
                struct sockaddr_in addrcli = {};
                socklen_t addrlen = sizeof (addrcli);        // 必须初始化;
                // 接收请求
                ssize_t rb = recvfrom (sockfd, buf, sizeof (buf), 0,    // 0 阻塞模式
                    (struct sockaddr*)&addrcli, &addrlen);
                if (rb == -1) {
                    perror ("recvfrom");
                    return -1;
                }
                printf ("服务器:向%s:%u客户机发送响应...\n",
                    inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
                // 发送响应
                if (sendto (sockfd, buf, rb, 0,                // 0 阻塞模式
                    (struct sockaddr*)&addrcli, addrlen) == -1) {
                    perror ("sendto");
                    return -1;
                }
            }
            // 第四步:关闭套接字;
            printf ("服务器:关闭套接字...\n");
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("服务器:大功告成!\n");
            return 0;
        }
    
    范例:udpcli.c
        #include <stdio.h>
        #include <string.h>
        #include <sys/socket.h>
        #include <netinet/in.h>
        #include <arpa/inet.h>
        int main (int argc, char* argv[]) {
            if (argc < 3) {
                fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
                return -1;
            }
            // 第一步:创建套接字;
            printf ("客户机:创建网络数据报套接字...\n");
            int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
            if (sockfd == -1) {
                perror ("socket");
                return -1;
            }
            // 第二步:准备地址(不需要绑定)
            printf ("客户机:准备地址...\n");
            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons (atoi (argv[2]));
            addr.sin_addr.s_addr = inet_addr (argv[1]);
            // 第三步:发送请求并响应;
            printf ("客户机:发送请求并接收响应...\n");
            for (;;) {
                printf ("%s:%u> ", inet_ntoa (addr.sin_addr),
                    ntohs (addr.sin_port));
                char buf[1024];
                gets (buf);
                if (! strcmp (buf, "!"))        // 叹号关闭
                    break;
                if (sendto (sockfd, buf, (strlen (buf) + 1) * sizeof (buf[0]), 0,
                (struct sockaddr*)&addr, sizeof (addr)) == -1) {
                    perror ("send");
                    return -1;
                }
                struct sockaddr_in addrsvr = {};
                socklen_t addrlen = sizeof (addrsvr);
                if (recvfrom (sockfd, buf, sizeof (buf), 0,
                    (struct sockaddr*)&addrsvr, &addrlen) == -1) {
                    perror ("recv");
                    return -1;
                }
                printf ("%s:%u< %s\n", inet_ntoa (addrsvr.sin_addr),
                    ntohs (addrsvr.sin_port), buf);
            }
            // 第四步:关闭套接字;
            printf ("客户机:关闭套接字...\n");
            if (close (sockfd) == -1) {
                perror ("close");
                return -1;
            }
            printf ("客户机:大功告成!\n");
            return 0;
        }

    练习:基于TCP协议的网络银行。
    代码:参见 /项目/bank2   
0 0
原创粉丝点击