返回介绍

套接字编程 (1)

发布于 2024-10-04 12:28:35 字数 21884 浏览 0 评论 0 收藏 0

0x11-套接字编程-1

套接字编程

  • 两种协议 TCPUDP
    • 前者可以理解为有保证的连接,后者是追求快速的连接
    • 当然最后一点有些 太过绝对 ,但是现在不需熬考虑太多,因为初入套接字编程,一切从简
    • 稍微试想便能够大致理解, TCP 追求的是可靠的传输数据, UDP 追求的则是快速的传输数据
    • 前者有繁琐的连接过程,后者则是根本不建立可靠连接(不是绝对),只是将数据发送而不考虑是否到达。

以下例子以 *nix 平台的便准为例,因为 Windows平台需要考虑额外的加载问题,稍作添加就能在 Windows 平台上运行

UDP

  • UDP

    • 这是一个十分简洁的连接方式,假设有两台主机进行通信,一台只发送,一台只接收。
    • 接收端:

          int sock; /* 套接字 */
          socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */
          char mess[15];
          char get_mess[GET_MAX]; /* 后续版本使用 */
          struct sockaddr_in recv_host, send_host;
      
          /* 创建套接字 */
          sock = socket(PF_INET, SOCK_DGRAM, 0);
      
          /* 把IP 和 端口号信息绑定在套接字上 */
          memset(&recv_host, 0, sizeof(recv_host));
          recv_host.sin_family = AF_INET;
          recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
          recv_host.sin_port = htons(6000); /* 使用6000 端口号 */
          bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
      
          /* 进入接收信息的状态 */
          recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
      
          /* 接收完成,关闭套接字 */
          close(sock);
      

      上述代码省略了许多必要的 错误检查 ,在实际编写时要添加

    • 代码解释:
      1. PF_INET 代表协议的类型,此处代表 IPv4 网络协议族, 同样 PF_INET6 代表 IPv6 网络协议族,这个范围在后方单独记录,不与IPv4混在一起(并不意味着更复杂,实际上更简便)。
      2. AF_INET 代表地址的类型,此处代表 IPv4 网络协议使用的地址族, 同样有 AF_INET6 (在操作系统实现中 PF_INET 和 AF_INET 的值一样,但是还是要写宏更好,而不应该直接用数字或者,混淆使用)
      3. htonlhtons 两个函数的使用涉及到 大端小端问题, 这里不叙述,需要记住的是在网络编程时一定要使用这种函数将必要信息转为 大端表示法
      4. (struct sockaddr *) 这个强制转换是为了参数的必须,但不会出错,因为 sizeof(struct sockaddr_in) == sizeof(struct sockaddr) 具体可以查询相关信息,之所以这么做是为了方便编写套接字程序的程序员。
    • 发送端:

          int sock;
          const char* mess = "Hello Server!";
          char get_mess[GET_MAX]; /* 后续版本使用 */
          struct sockaddr_in recv_host;
          socklen_t addr_len;
          /* 创建套接字 */
          sock = socket(PF_INET, SOCK_DGRAM, 0);
          /* 绑定 */
          memset(&recv_host, 0, sizeof(recv_host));
          recv_host.sin_family = AF_INET;
          recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
          recv_host.sin_port = htons(6000);
          /* 发送信息 */
          /* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */
          sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
          /* 完成,关闭 */
          close(sock);
      

      上述代码是发送端。

    • 代码解释:
      1. inet_addr 函数是用于将字符串格式的 IP地址 转换为 大端表示法的 地址类型,即 s_addr 的类型 in_addr_t
      2. 与之相反,同样也有功能相反的函数 inet_ntoa 用于将 in_addr_t 类型转为字符串,但是使用时一定要记住及时拷贝返回值
        char addr[16];
        recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
        strcpy(addr, inet_ntoa(recv_host.sin_addr.s_addr));
        
    • 从上述代码看出, UDP 协议的使用十分简洁,几乎就是 创建套接字->准备数据->装备套接字->发送/接收->结束
    • 其中,都没有连接的操作,但是实际上这是为了方便 UDP 随时和 不同的主机 进行通信所默认的设置,如果需要和相同主机一直通信呢?
    • 此中的原由暂时不需要知道,记录方法,即长时间使用 UDP 和同一主机通信时,可以使用 connect 函数来进行优化自身。此时 假设两台主机的实际功能一致,既接收也发送
    • 发送端:

          /* 前方高度一致,将 bind函数替换为 */
          connect(sock, (struct sockaddr *)&recv_host, sizeof(recv_host); // 将对方的 IP地址和 端口号信息 注册进UDP的套接字中)
          while(1) /* 循环的发送和接收信息 */
          {
            size_t read_len = 0;
            /* 原先使用的 sendto 函数,先择改为使用 write 函数, Windows平台为 send 函数 */
            write(sock, mess, strlen(mess));            /* send(sock, mess, strlen(mess), 0) FOR Windows Platform */
            read_len = read(sock, get_mess, GET_MAX-1); /* recv(sock, mess, strlen(mess)-1, 0) FOR Windows Platform */
            get_mess[read_len-1] = '\0';
            printf("In Client like Host Recvive From Other Host : %s\n", get_mess);
          }
          /* 后方高度一致 */
      
    • 接收端:

          /* 前方一致, 添加额外的 struct sockaddr_in send_host; 并添加循环,构造收发的现象*/
              while(1)
              {
                size_t read_len = 0;
                char sent_mess[15] = "Hello Sender!"; /* 用于发送的信息     */
                sendto(sock, mess, strlen(sent_mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
                read_len = recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len)
                mess[read_len-1] = '\0';
                printf("In Sever like Host Recvive From other Host : %s\n", mess);
              }
              /* 后方高度一致 */
              /*
              * 之所以只在接收端使用 connect 的原因,便在于我们模拟的是 客户端-服务器 的模型,而服务器的各项信息是不会随意变更的
              * 但是 客户端就不同了,可能由于 ISP(Internet Server Provider) 的原因,你的IP地址不可能总是固定的,所以只能
              * 保证 在客户端 部分注册了 服务器 的各类信息,而不能在 服务器端 注册 客户端 的信息。
              * 当然也有例外,例如你就想这个软件作为私密软件,仅供两个人使用, 且你有固定的 IP地址,那么你可以两边都connect,但是
              * 一定要注意,只要有一点信息变动,这个软件就可能无法正常的收发信息了。
              */
      
  • 代码解释
    • 故而实际上,虽然前方的表格显示,UDP 似乎并没有 connect 的使用必要,但是实际上还是有用到的地方。
    • *nixAPI 来说,sendtowrite 的区别十分明显,便是一个需要在参数中提供目标主机的各类信息,而后者则不需要提供。同样的道理recvfromread也是如此。
    • 这个代码只是做演示而已,所以将代码置于无限循环当中,现实中可以自行定义出口条件。

以上是 UDP 的一些简单说明,入门足矣,并未详细叙述某些 函数 的具体用法,而是用实际例子来体现。 在 记录 TCP 之前,还是需要讲一个函数 shutdown

  • shutdownclose(closesocket)

    • 首先要知道,网络通信一般而言是双方的共同进行的,换而言之就是双向的,一个方向只用来发送消息,一个方向只用来读取消息。
    • 这就导致了,在结束套接字通信的时候,需要关闭两个方向的通道(暂时叫它们通道),那同时关闭不行吗?可以啊
      • close(sock); // closesocket(sock); FOR Windows PlatForm 就是这么干的,同时断开两个方向的连接。
      • 简单的通信程序或者单向通信程序这么做的确无甚大碍,但是万一在结束通信的时候需要接收最后一个信息那该怎么办?
        • 假设通信结束,客户端向服务器发送 "Thank you"
        • 服务器需要接收这个信息,之后才能关闭通信
        • 问题就在这之间,服务器并不知道客户端会在通信结束后的什么时刻传来信息
        • 所以我们选择在通信完成后先关闭 服务器的 发送通道(写流),等待客户端发来消息后,关闭剩下的 接收通道(读流)
    • 发送端:

          /* 假设有一个 TCP 的连接,此为客户端 */
          write(sock, "Thank you", 10);
          close(sock); // 写完直接关闭通信
      
    • 接收端:

          /* 此为服务器 */
          /* 首先关闭写流 */
          shutdown(sock_c, SHUT_WR);
          read(sock_c, get_mess, GET_MAX);
          printf("Message : %s\n", get_mess);
          close(sock_c);
          close(sock_s); // 关闭两个套接字是因为 TCP 服务器端的需要,后续会记录
      
    • 代码解释
      • shutdown 函数的作用就是 可选择的关闭那个方向的输出
        • int shutdown(int sock, int howto);
        • sock 代表要操作的套接字
        • howto有几个选择
          • *nix : SHUT_RD SHUT_WR SHUT_RDWR
          • Windows : SD_RECEIVE SD_SEND SD_BOTH

停下来

  1. 程序员应该越来越来,做的事情应该越来越少,但是能达到的成就应该越来越多
  2. 在 IPv6 出现的今天,网络编程已经开始向简洁和强大靠近,即便是身为底层语言的 C语言
  3. 实际上由于 C语言 并没有自己的网络库, 故为了能进行网络编程,不得不依赖于系统函数,这就是所谓的系统编程, 你已经是一个系统程序员了。
  4. 而 系统函数 随着时代的变化,正在不断完善,增加(几乎没有废除的先例,所以不用担心之前的程序无法运行)。
  5. 相应的,由于以前的网络编程只适合于 IPv4 的地址,自从出现了 IPv6, 我们需要一套全新的方式,正好他来了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文