写点什么

Android C++ 系列:Linux Socket 编程(三)CS 模型示例

作者:轻口味
  • 2021 年 12 月 15 日
  • 本文字数:3219 字

    阅读完需:约 11 分钟

Android C++系列:Linux Socket编程(三)CS模型示例

1. TCP 通信

下图是基于 TCP 协议的客户端/服务器程序的一般流程:



服务器调用 socket()、bind()、listen()完成初始化后,调用 accept()阻塞等待,处于 监听端口的状态,客户端调用 socket()初始化后,调用 connect()发出 SYN 段并阻塞等待服 务器应答,服务器应答一个 SYN-ACK 段,客户端收到后从 connect()返回,同时应答一个 ACK 段,服务器收到后从 accept()返回。


数据传输的过程:


建立连接后,TCP 协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是 由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从 accept() 返回后立刻调用 read(),读 socket 就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用 write()发送请求给服务器,服务器收到后从 read()返回,对客户端的请求进行处 理,在此期间客户端调用 read()阻塞等待服务器的应答,服务器调用 write()将处理结果发 回给客户端,再次调用 read()阻塞等待下一条请求,客户端收到后从 read()返回,发送下一 条请求,如此循环下去。


如果客户端没有更多的请求了,就调用 close()关闭连接,就像写端关闭的管道一样, 服务器的 read()返回 0,这样服务器就知道客户端关闭了连接,也调用 close()关闭连接。注 意,任何一方调用 close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方 调用 shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。


在学习 socket API 时要注意应用程序和 TCP 协议层是如何交互的:


  • 应用程序调用某个 socket 函数时 TCP 协议层完成什么动作,比如调用 connect()会发出 SYN 段

  • 应用程序如何知 道 TCP 协议层的状态变化,比如从某个阻塞的 socket 函数返回就表明 TCP 协议收到了某些段, 再比如 read()返回 0 就表明收到了 FIN 段

1.1 server

下面通过最简单的客户端/服务器程序的实例来学习 socket API。 server.c 的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。


/* server.c */#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define MAXLINE 80 #define SERV_PORT 8000int main(void) {  struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len;  int listenfd, connfd;  char buf[MAXLINE];  char str[INET_ADDRSTRLEN]; int i, n;  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);  bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));   listen(listenfd, 20);  printf("Accepting connections ...\n");   while (1) {    cliaddr_len = sizeof(cliaddr);    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);    n = read(connfd, buf, MAXLINE);     printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));    for (i = 0; i < n; i++)       buf[i] = toupper(buf[i]);    write(connfd, buf, n);    close(connfd);   }}
复制代码

1.2 client

client.c 的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的 字符串并打印。


/* client.c */#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h>#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char *argv[]) {  struct sockaddr_in servaddr;   char buf[MAXLINE];  int sockfd, n;  char *str;  if (argc != 2) {    fputs("usage: ./client message\n", stderr); exit(1);  }  str = argv[1];  sockfd = socket(AF_INET, SOCK_STREAM, 0);  bzero(&servaddr, sizeof(servaddr));   servaddr.sin_family = AF_INET;  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);   servaddr.sin_port = htons(SERV_PORT);  connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));  write(sockfd, str, strlen(str));  n = read(sockfd, buf, MAXLINE);   printf("Response from server:\n"); write(STDOUT_FILENO, buf, n);  close(sockfd);  return 0; }
复制代码


由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用 bind(),只是没有必要调用 bind()固定一个端口号,服务器也不是必须调用 bind(),但如果服务器不调用 bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。 客户端和服务器启动后可以查看链接情况:


 netstat -apn|grep 8000
复制代码

2. UDP


由于 UDP 不需要维护连接,程序逻辑简单了很多,但是 UDP 协议是不可靠的,实际上有很 多保证通讯可靠性的机制需要在应用层实现。


编译运行 server,在两个终端里各开一个 client 与 server 交互,看看 server 是否具有并 发服务的能力。用 Ctrl+C 关闭 server,然后再运行 server,看此时 client 还能否和 server 联 系上。和前面 TCP 程序的运行结果相比较,体会无连接的含义。

2.1 server

/* server.c */#include <stdio.h>#include <string.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(void) {  struct sockaddr_in servaddr, cliaddr;   socklen_t cliaddr_len;  int sockfd;  char buf[MAXLINE];  char str[INET_ADDRSTRLEN];   int i, n;  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, (struct sockaddr *)&servaddr, sizeof(servaddr));  printf("Accepting connections ...\n");   while (1) {    cliaddr_len = sizeof(cliaddr);    n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);     if (n == -1)      perr_exit("recvfrom error");     printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));    for (i = 0; i < n; i++)       buf[i] = toupper(buf[i]);    n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));     if (n == -1)      perr_exit("sendto error");  } }
复制代码

2.2 client

/* client.c */#include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80#define SERV_PORT 8000int main(int argc, char *argv[]) {  struct sockaddr_in servaddr;   int sockfd, n;  char buf[MAXLINE];  char str[INET_ADDRSTRLEN];   socklen_t servaddr_len;  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);  bzero(&servaddr, sizeof(servaddr));   servaddr.sin_family = AF_INET;  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);   servaddr.sin_port = htons(SERV_PORT);  while (fgets(buf, MAXLINE, stdin) != NULL) {    n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));     if (n == -1)      perr_exit("sendto error");    n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);     if (n == -1)      perr_exit("recvfrom error");    Write(STDOUT_FILENO, buf, n);   }  Close(sockfd);  return 0; }
复制代码

3. 总结

本文分别介绍了基于 TCP 和 UDP 的客户端和服务器的开发流程及对应实现。

发布于: 46 分钟前阅读数: 4
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017.10.17 加入

Android、音视频、AI相关领域从业者。 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论
Android C++系列:Linux Socket编程(三)CS模型示例