网络编程笔记(四)-套接字选项和 UDP

网络编程笔记(四)-套接字选项和 UDP

参考《UNIX网络编程》第 7、8 章,《TCP/IP 网络编程》 第 9 章。

套接字选项

有很多方法来获取和设置影响套接字的选项:

  • getsockopt 和 setsockopt 函数;
  • fcntl 函数;
  • ioctl 函数。

getsockopt 和 setsockopt

#include <sys/socket.h>
// 均返回:若成功则为0,若出错则为-1.
int getsockopt(int sockfd int level, int optname, void *optval, socklen_t *optlen);  
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数:

  1. sockfd:指向一个打开的套接字描述符
  2. level:指定系统中解释选项的代码或为通用套接字代码,或为某个特定于协议的代码(IPV4、IPV6、TCP、SCTP)。
  3. optname:选项名。
  4. *optval:指向某个变量的指针。对于 setsockopt 它是值参数,从 *optval 中取得选项待设置的新值。对于 getsockopt 它是值-结果参数,把已获取的选项当前值存放到 *optval 中。
  5. optlen:*optval 的大小由它指定。

套接字选项粗分为两大基本类型:

  1. 标志选项:启用或禁止某个特性的二元选项。*optval 为 0 表示禁止,不为 0 表示启用。
  2. 值选项:取得并返回我们可以设置或检查的特定值的选项。

一些套接字选项

SO_RCVBUF & SO_SNDBUF:I/O 缓冲大小相关

  • SO_RCVBUF:输入缓冲大小相关可选项
  • SO_SNDBUF:输出缓冲大小相关可选项
n = 220 * 1024;
setsockapt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

对于 TCP 来说,套接字接收缓冲区中可用空间的的大小限定了 TCP 通告对端的窗口大小。TCP 套接字接收缓冲区不可能溢出,因为不允许对端发出超过本端所通告窗口大小的数据。

UDP 是没有流量控制的:较快地发送端可以很容易地淹没较慢地接收端。

SO_REUSEADDR:重用端口号

SO_REUSEADDR 允许 TIME-WAIT 状态下套接字端口号被重用。

它与四次握手中的 TIME-WAIT 状态有关:若服务器先断开连接,则无法立即重新运行,因为套接字在 TIME-WAIT 状态时相应端口号被占用,bind 会失败。下面的代码解决了这个问题。

optlen = sizeof(option);  
option = TRUE;  
setsockopt(serv_sock, SOL_SOCKET, SO_RESUREADDR, (void *)&option, optlen);  

7.5.11 P165:所有 TCP 服务器都应该指定本套接字选项,以允许服务器重新启动时 bind 成功。

补充:为什么需要 TIME-WAIT:

image

在 TIME-WAIT 状态,A 可以继续接收 B 的信息。如果没有 TIME-WAIT,A 发送 ACK 后立即消除套接字,但是 ACK 丢失了,则 B 永远无法接收到 A 的 ACK,会不断试图重传 FIN(B 认为自己发送的 FIN 没有到达 A)。

TCP_NODELAY

Nagle 算法:防止数据包过多造成网络过载。

  1. 立即发送一个数据段,即使发送缓冲区只有一个字节。
  2. 只有收到上一个数据段的确认或者发送缓冲区中数据超过 MSS,才可以发送下一个数据段。
  3. 对于即时性要求高的地方,如,Window方式的鼠标操作,要关闭Nagle 算法。

禁用 Nagle 算法只需将套接字可选项 TCP_NODELAY 改为 1。

int opt_val = 1;	// 禁用 Nagle 算法
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));

fcntl 函数

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, .../* int arg */);

fcntl (file control) 可执行各种描述符控制操作,提供了与网络编程相关的如下特性:

  • 非阻塞式 I/O:通过使用 F_SETFL 命令设置 O_NONBLOCK 文件状态标志,我们可以把一个套接字设置为非阻塞型。
  • 信号驱动 I/O:通过使用 F_SETFL 命令设置 O_ASYNC 文件状态标志,我们可以把一个套接字设置成一旦其状态发生变化,内核就产生一个 SIGIO 信号。
  • F_SETOWN 命令允许我们指定用于接收 SIGIO 和 SIGURG 信号的套接字属主(进程 ID 或进程组 ID)。

总结

  • 许多 TCP 服务器设置 SO_KEEPALIVE 套接字选项以自动终止一个半开连接。
  • SO_LINGER 套接字使得我们能够更好的地控制 close 函数的返回时机,允许我们强制发送 RST 而不是 TCP 的四分组连接终止序列。
  • 每个 TCP 套接字和 SCTP 套接字都有一个发送缓冲区和接收缓冲区,每个 UDP 套接字都有一个接收缓冲区。SO_SNDBUF 和 SO_RCVBUF 套接字选项允许我们改变这些缓冲区的大小。这两个选项最常见的用途是长肥管道上的批量数据传送。

基本 UDP 套接字编程

UDP 客户/服务器原理

UDP是无连接不可靠的数据报协议。典型的 UDP 客户/服务器程序中,客户不与服务器建立连接,而是只管使用 sendto 给服务器发送数据报;服务器也不接受来自客户的连接,只管调用 recvfrom 函数等待客户数据的到达。

使用 UDP 编写的一些常见应用程序有:DNS(域名系统)、NFS(网络文件系统)、SNMP(简单网络管理协议)。

image

recvfrom 和 sendto 函数

#include <sys/socket.h>

// 均返回:若成功则返回读或写的字节大小,若出错则返回-1
ssize_t recvform(int sockfd, void *buff , size_t nbytes, int flags, 
	struct sockaddr *from , socklen_t *addrlen);

ssize_t sendto(int sockfd, const void *buff , size_t nbytes, int flags, 
	const struct sockaddr *to , socklen_t addrlen);

参数:

  • 前三个参数 sockfd、buff、nbytes 等同于 read 和 write 的三个参数:描述符、指向读入/写出缓冲区的指针、读/写字节数。
  • flags:这里暂时把 flags 置为 0。
  • recvfrom 的 from:指向一个由该函数返回时候填写的数据报发送者的协议地址的套接字地址结构,填写的字节数在 addrlen(值-结果参数)。recvfrom 的参数 from、addrlen 类似 accept 的最后两个参数。
  • sendto 的 to:指向一个含有数据报接收者的协议地址(IP 地址和端口号)的套接字地址结构,大小由 addrlen 参数指定。sendto 的参数 to、addrlen 类似 connect 的最后两个参数。

UDP 回射服务程序和客户程序

服务程序 udpcliserv/udpserv01.c

#include "../lib/unp.h"

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen) {
  int n;
  socklen_t len;
  char mesg[MAXLINE];

  for (;;) {
    len = clilen;
    n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
    Sendto(sockfd, mesg, n, 0, pcliaddr, len);
  }
}

int main(int argc, char **argv) {
  int sockfd;
  struct sockaddr_in servaddr, cliaddr;

  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);

  Bind(sockfd, (SA *)&servaddr, sizeof(servaddr));

  dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));
}

客户程序 udpcliserv/udpcli01.c

#include "../lib/unp.h"


void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) {
  int n;
  char sendline[MAXLINE], recvline[MAXLINE + 1];

  while (Fgets(sendline, MAXLINE, fp) != NULL) {
    Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
    // 注意recvfrom的最后两个参数为NULL,不关心数据发送者的协议地址
	n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);	

    recvline[n] = 0; /* null terminate */
    Fputs(recvline, stdout);
  }
}

int main(int argc, char **argv) {
  int sockfd;
  struct sockaddr_in servaddr;

  if (argc != 2) err_quit("usage: udpcli <IPaddress>");

  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(SERV_PORT);
  Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

  dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr));

  exit(0);
}

大多数 TCP 服务器是并发的,大多数 UDP 服务器是迭代的。

每个 UDP 套接字都有一个接收缓冲区,当进程调用 recvfrom 时,缓冲区中的下一个数据报以 FIFO (先进先出)顺序返回给进程。

image

UDP 初始回射程序的问题

  1. 数据报丢失
    数据报丢失导致客户永久阻塞于 recvfrom 调用,设置一个超时可以解决这个问题。

  2. 验证接收到的响应:
    重写 dg_cli 函数,分配另一个套接字地址结构用于存放由 recvfrom 返回的结构,然后比较返回的地址。保留来自数据报所发往的服务器的应答,而忽略任何其他数据报。

    #include "../lib/unp.h"
    
    void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) {
      int n;
      char sendline[MAXLINE], recvline[MAXLINE + 1];
      socklen_t len;
      struct sockaddr *preply_addr;
    
      preply_addr = Malloc(servlen);	// 分配另一个套接字地址结构
    
      while (Fgets(sendline, MAXLINE, fp) != NULL) {
        Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
    
        len = servlen;
        // 注意这里与 dg_cli 相比,recvfrom的最后2个参数不为0,设为新分配的套接字地址
        n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
        // 比较返回的地址
        if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) {
          printf("reply from %s (ignored)\n", Sock_ntop(preply_addr, len));
          continue;
        }
    
        recvline[n] = 0; /* null terminate */
        Fputs(recvline, stdout);
      }
    }
    
  3. 服务器进程未运行

    异步错误:服务器主机响应 “port unreachable” ICMP消息,不过这个 ICMP 错误不返回给客户进程,客户永远阻塞于 recvfrom 调用。

    对于一个 UDP 套接字,由它引发的异步错误却并不返回给它,除非它已连接。这需要给 UDP 套接字调用 connect。

UDP 的 connect 函数

UDP 的 connect 不同于TCP,没有三次握手的过程,内核只是检查是否存在立即可知的错误,记录对端 IP 地址和端口号,然后立即返回到调用进程。对于已连接的 UDP 套接字,与默认未连接的 UDP 套接字相比,有以下 3 个变化:

  1. 不能再给输出操作指定目的 IP 地址和端口号——不使用 sendto,而改用 write 或 send。
  2. 不必使用 recvfrom 以获悉数据报的发送者,而改用 read、recv 或 recvmsg。
  3. 已连接 UDP 套接字引发的异步错误会返回给它们所在的进程,而未连接的 UDP 套接字不接收任何异步错误。

image

image

小结:UDP 客户进程或服务器进程只在使用自己的 UDP 套接字与确定的唯一对端通信时,才可以调用 connect。调用 connect 通常是 UDP 客户。

将初始的 UDP 客户函数 dg_cli 修改:调用 connect,并以 read 和 write 代替 recvfrom 和 sendto。

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) {
  int n;
  char sendline[MAXLINE], recvline[MAXLINE + 1];

  Connect(sockfd, (SA *)pservaddr, servlen);

  while (Fgets(sendline, MAXLINE, fp) != NULL) {
    Write(sockfd, sendline, strlen(sendline));
    n = Read(sockfd, recvline, MAXLINE);

    recvline[n] = 0; /* null terminate */
    Fputs(recvline, stdout);
  }
}

使用 select 函数的 TCP 和 UDP 回射服务器程序

/* include udpservselect01 */
#include "../lib/unp.h"

int main(int argc, char **argv) {
  int listenfd, connfd, udpfd, nready, maxfdp1;
  char mesg[MAXLINE];
  pid_t childpid;
  fd_set rset;
  ssize_t n;
  socklen_t len;
  const int on = 1;
  struct sockaddr_in cliaddr, servaddr;
  void sig_chld(int);

  /* create listening TCP socket */
  listenfd = Socket(AF_INET, SOCK_STREAM, 0);

  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);

  Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); // 设置SO_REUSEADDR套接字选项以防该端口已有连接存在。
  Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));

  Listen(listenfd, LISTENQ);

  /* create UDP socket */
  udpfd = Socket(AF_INET, SOCK_DGRAM, 0);

  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);

  // 无需调用bind之前设置SO_REUSEADDR套接字选项,因为TCP端口独立于UDP端口
  Bind(udpfd, (SA *)&servaddr, sizeof(servaddr));	
  /* end udpservselect01 */

  /* include udpservselect02 */
  Signal(SIGCHLD, sig_chld); /* must call waitpid() */

  FD_ZERO(&rset);
  maxfdp1 = max(listenfd, udpfd) + 1;
  for (;;) {
    FD_SET(listenfd, &rset);
    FD_SET(udpfd, &rset);
    if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {
      if (errno == EINTR)
        continue; /* back to for() */
      else
        err_sys("select error");
    }

    if (FD_ISSET(listenfd, &rset)) {
      len = sizeof(cliaddr);
      connfd = Accept(listenfd, (SA *)&cliaddr, &len);

      if ((childpid = Fork()) == 0) { /* child process */
        Close(listenfd);              /* close listening socket */
        str_echo(connfd);             /* process the request */
        exit(0);
      }
      Close(connfd); /* parent closes connected socket */
    }

    if (FD_ISSET(udpfd, &rset)) {
      len = sizeof(cliaddr);
      n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *)&cliaddr, &len);

      Sendto(udpfd, mesg, n, 0, (SA *)&cliaddr, len);
    }
  }
}
/* end udpservselect02 */

总结

  • UDP 套接字可能产生异步错误(分组发送完一段时间后才报告的错误),TCP 套接字总是给应用进程报告这些错误,但是 UDP 套接字必须已连接才能接收这些错误。

  • UDP 没有流量控制,但这一般不成问题,因为许多 UDP 应用程序是用请求-应答模式构造的,而且不用传送大量数据。

参考资料

https://wuhlan3.gitee.io/wuhlan3/2021/08/06/UNIX网络编程(七)

https://blog.csdn.net/zzxiaozhao/article/details/102662861

posted @ 2021-10-07 12:33  CoolGin  阅读(297)  评论(0编辑  收藏  举报