概述

socket(套接字)通信主要用于运行在不同服务器上的进程之间通信(服务器通过网络相连),也可以用于在同一服务器上的进程之间通信。套接字就相当于端与端之间的独立的门,通信就通过这个门来进行。

创建套接字

创建成功返回一个套接字描述符,基本上可以把它看成是一个文件描述符,很多文件描述符的api,对套接字描述符也是适用的。

  1. #include <sys/socket.h>
  2. int socket(int domain, int type, int protocol);

domain

决定了通信的性质,这些性质包括地址格式等,一般有以下值:

  • AF_INET:IPv4 因特网域。
    AF_INET6:IPv6 因特网域。
  • AF_UNIX: UNIX 域。大多系统还定义了AF_LOCAL域,它是AF_UNIX的别名。
  • AF_UNSPEC: 未指定,这是一个可以表达任何域的通配符。

type

参数的类型,一般有以下:

  • SOCK_DGRAM:固定长度,无连接,不可靠的消息。
  • SOCK_RAW: IP 协议的数据报接口。(在 POSIX.1 中是可选的)
  • SOCK_SEQPACKET: 固定长度,顺序式,面向连接的,可靠的消息。
  • SOCK_STREAM: 顺序式,面向连接的,双向的,可靠的字节流。

    protocol

    通常是0,表示为给定的域和套接字类型的组合选择默认协议。当一对域和套接字类型支持多个协议时,可用protocol参数选择一个特定协议。AF_INET通信域和SOCK_STREAM套接字类型的默认协议是TCP。AF_INET通信域和SOCK_DGRAM套接字类型的默认协议是UDP。以下是protocol参数的可选值:

  • IPPROTO_IP :IPv4网络协议

  • IPPROTO_IPV6 :IPv6网络协议
  • IPPROTO_ICMP :因特网控制报文协议
  • IPPROTO_RAW: 原始数据包协议
  • IPPROTO_TCP: 传输控制协议
  • IPPROTO_UDP: 用户数据报协议

绑定网络域

当通过socket函数获得套接字描述符后,对于服务端来讲,首先就是和一个具体的网络地址绑定,这个套接字一般称为监听套接字,其函数为:

  1. #include <sys/socket.h>
  2. int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
  • sockfd:套接字描述符
  • addr:绑定的网络域
  • len:addr的长度

sockaddr

一个const struct sockaddr *指针, 绑定套接字的网络域,不同的网络域具有不同的地址格式,sockaddr 是地址的通用结构体:

  1. struct sockaddr {
  2. sa_family_t sa_family;/* address family */
  3. char sa_data[];/* variable-length address */
  4. .
  5. .
  6. .
  7. };

这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:

  1. struct sockaddr_in {
  2. sa_family_t sin_family; /* address family: AF_INET */
  3. in_port_t sin_port; /* port in network byte order */
  4. struct in_addr sin_addr; /* internet address */
  5. };
  6. /* Internet address. */
  7. struct in_addr {
  8. uint32_t s_addr; /* address in network byte order */
  9. };

如ipv6对应的是:

  1. struct sockaddr_in6 {
  2. sa_family_t sin6_family; /* AF_INET6 */
  3. in_port_t sin6_port; /* port number */
  4. uint32_t sin6_flowinfo; /* IPv6 flow information */
  5. struct in6_addr sin6_addr; /* IPv6 address */
  6. uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
  7. };
  8. struct in6_addr {
  9. unsigned char s6_addr[16]; /* IPv6 address */
  10. };

如unix对应的是:

  1. #define UNIX_PATH_MAX 108
  2. struct sockaddr_un {
  3. sa_family_t sun_family; /* AF_UNIX */
  4. char sun_path[UNIX_PATH_MAX]; /* pathname */
  5. };

监听套接字

服务端针对面向连接的套接字进行监听,保证连接的有效性。服务端的监听函数如下:

  1. int listen(int sockfd, int backlog);

backlog 参数表示能够排队最大连接请求数量,但实际最大的值的上限由系统决定,毕竟不可能设置一个超越系统配置的数量。
很明显,这个参数表明了内核需要维护一个存储与客户端连接关系的队列。

接受请求

服务端通过监听函数可以发现那些发起连接请求的客户端,而accept函数是去为这些客户端建立起真正的连接关系,函数如下:

  1. int accept(int sockfd, struct sockaddr *restrict addr,socklen_t *restrict len);

accept默认会阻塞进程,直到有一个客户连接建立后返回,若函数成功返回,返回值是一个由内核自动生成的全新的套接字,代表与客户端的连接。也就是说,服务端每有新的客户端连接都会新建一个套接字。

连接网络域

当通过socket函数获得套接字描述符后,对于客户端来讲,就是可以与服务端具体的网络域进行连接了,函数如下:

  1. int connect(int sockfd, const struct sockaddr *addr, socklen_t len);

发送消息

  1. ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags)
  2. ssize_t sendto(int sockfd, const void *buf, size t nbytes, int flags,
  3. const struct sockaddr *destaddr, socklen t destlen);
  4. ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

flag: 0 阻塞、1 不阻塞

接收消息

  1. ssize_t recv(int sockfd, void *buf, size t nbytes, int flags):
  2. ssize_t recvfrom(int sockfd, void *restrict buf, size t len, int flags,
  3. struct sockaddr *restrict addr,socklen t *restrict addrlen);
  4. ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

flag: 0 阻塞、1 不阻塞

关闭套接字的连接

  1. int shutdown (int sockfd, int how);

how是指关闭的方式:

  • SHUT_RD: 关闭读。
  • SHUT_WR: 关闭写。
  • SHUT_RDWR: 关闭读写。

例子

因为基于SOCK_DGRAM类型的套接字比较简单,所以直接搞个SOCK_STREAM的一步到位。
服务端建立一个面向连接的套接字,去接收多个客户端的消息。客户端输入”end”结束连接。

服务端

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <sys/socket.h>
  5. #include <sys/types.h>
  6. #include <string.h>
  7. #include <arpa/inet.h>
  8. int main(int argc, char *argv[])
  9. {
  10. struct sockaddr_in clientaddr, serveraddr;
  11. int sockfd;
  12. int connectSockfd;
  13. int addrlen;
  14. pid_t pid;
  15. // 创建socket文件描述符用于处理监听
  16. if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  17. {
  18. perror("socket create failed!");
  19. exit(1);
  20. }
  21. // 初始化服务端的网络域,包括地址族,端口,和IP地址
  22. bzero(&serveraddr, sizeof(serveraddr));
  23. serveraddr.sin_family = AF_INET;
  24. serveraddr.sin_port = htons(atoi(argv[2]));
  25. inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);
  26. // 将sockfd文件描述符和服务器地址绑定
  27. if ((bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))) < 0)
  28. {
  29. perror("bind error");
  30. close(sockfd);
  31. exit(1);
  32. }
  33. // 监听,设置最大连接为1,测试超过1时会发生什么
  34. if (listen(sockfd, 1) < 0)
  35. {
  36. perror("listen error");
  37. close(sockfd);
  38. exit(1);
  39. }
  40. printf("waiting client connecting\n");
  41. while (1)
  42. {
  43. // 处理客户端连接,返回一个与客户端相关的套接字
  44. addrlen = sizeof(clientaddr);
  45. if ((connectSockfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
  46. {
  47. perror("accept error");
  48. continue;
  49. }
  50. if ((pid = fork()) < 0)
  51. {
  52. perror("listen error");
  53. close(sockfd);
  54. close(connectSockfd);
  55. exit(1);
  56. }
  57. // 父进程关连接fd,继续accept新的连接
  58. else if (pid > 0)
  59. {
  60. close(connectSockfd);
  61. }
  62. // 子进程关绑定fd,并处理当前连接fd的逻辑
  63. else
  64. {
  65. char buf[1024];
  66. char dst[128];
  67. close(sockfd);
  68. while (1)
  69. {
  70. memset(buf, 0, sizeof(buf));
  71. if ((recv(connectSockfd, buf, sizeof(buf), 0)) < 0)
  72. {
  73. perror("recv error");
  74. break;
  75. }
  76. printf("*-------------------------------------*");
  77. printf("---- from ip:%s\tport:%d ------\n", inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, dst, sizeof(dst)), ntohs(clientaddr.sin_port));
  78. if ((write(STDOUT_FILENO, buf, strlen(buf))) < 0)
  79. {
  80. perror("write error");
  81. break;
  82. }
  83. printf("*--------- msg recv success ------------*");
  84. // 发送消息
  85. if ((send(connectSockfd, "recv ok", 8, 0)) < 0)
  86. {
  87. perror("send error");
  88. break;
  89. }
  90. printf("*-------------------------------------*\n\n\n\n");
  91. }
  92. // 关闭用于监听的文件描述符
  93. close(connectSockfd);
  94. }
  95. }
  96. // 关闭用于监听的文件描述符
  97. close(sockfd);
  98. return 0;
  99. }

客户端

  1. #include <stdio.h>
  2. #include <sys/socket.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <string.h>
  6. #include <arpa/inet.h>
  7. int main(int argc, char *argv[])
  8. {
  9. struct sockaddr_in serveraddr;
  10. int sockfd;
  11. char buf[1024];
  12. char dst[128];
  13. // 创建用于连接服务器的sockfd
  14. if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  15. {
  16. perror("socket create failed!");
  17. exit(1);
  18. }
  19. // 初始化服务器端的地址信息,主机序转换为网络序
  20. bzero(&serveraddr, sizeof(serveraddr));
  21. serveraddr.sin_family = AF_INET;
  22. serveraddr.sin_port = htons(atoi(argv[2]));
  23. inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);
  24. // 连接服务器
  25. if ((connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))) < 0)
  26. {
  27. perror("connect error");
  28. close(sockfd);
  29. exit(1);
  30. }
  31. while (1)
  32. {
  33. printf("*------- please enter message to send: --------*\n");
  34. memset(buf, 0, sizeof(buf));
  35. // 读取控制台输入
  36. if ((read(STDOUT_FILENO, buf, sizeof(buf))) < 0)
  37. {
  38. perror("read error");
  39. break;
  40. }
  41. // 输入end结束输入
  42. if (strncmp(buf, "end", 3) == 0)
  43. {
  44. break;
  45. }
  46. // 发送
  47. if ((send(sockfd, buf, strlen(buf), 0)) < 0)
  48. {
  49. perror("read error");
  50. break;
  51. }
  52. printf("*------- msg send success! --------*\n\n\n");
  53. // 接收
  54. if ((recv(sockfd, buf, sizeof(buf), 0)) < 0)
  55. {
  56. perror("read error");
  57. break;
  58. }
  59. printf("------ from ip:%s\tport:%d -----\n", inet_ntop(AF_INET, &serveraddr.sin_addr.s_addr, dst, sizeof(dst)), ntohs(serveraddr.sin_port));
  60. // 打印到控制台
  61. if ((write(STDOUT_FILENO, buf, strlen(buf))) < 0)
  62. {
  63. perror("read error");
  64. break;
  65. }
  66. char dst[128];
  67. printf("\n*--------- msg recv success ! -----------*\n\n\n");
  68. }
  69. // 释放连接的文件描述符
  70. close(sockfd);
  71. return 0;
  72. }

其他

可使用tcpdump命令来监听两者之间的交互。

  • -i 指定网卡名称
  • host 指定host
  • port 指定端口
  • -vv 输出详细的报文信息
    1. tcpdump -vv -i lo host 127.0.0.1