Android系统原理性问题分析 - 单路情况下的C/S模型

news/2024/11/28 11:01:00/

声明

  • 在Android系统中经常会遇到一些系统原理性的问题,在此专栏中集中来讨论下。
  • Android系统中很多地方都采用了I/O多路复用的机制,为了引出I/O多路复用机制,先来分析多路并发情况下的C/S模型。
  • 此篇参考一些博客和书籍,代码基于Android 7.1.1,不方便逐一列出,仅供学习、知识分享。
  • 此篇代码下载:单路情况下的CS模型 tcp-socket udp-socket ud-socket

1 概述

在一个典型的客户端/服务器场景中,应用程序使用 socket 进行通信的方式如下:

  • 各个应用程序创建一个 socket。socket 是一个允许通信的“设备”,两个应用程序都需要用到它。
  • 服务器将自己的 socket 绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。

使用 socket()系统调用能够创建一个 socket,它返回一个用来在后续系统调用中引用该socket 的文件描述符。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);		//returns file descriptor on success, or -1 on error

不管参数protocol,此篇中protocol参数都设置为0,如果对这个参数感兴趣,可以自行搜索学习。主要分析下前两个参数: domain 和 type。

1.1 通信 domain

socket 存在于一个通信 domain 中,它确定:

  • 识别出一个 socket 的方法(即 socket“地址”的格式);
  • 通信范围(位于同一主机上的应用程序之间还是在位于使用一个网络连接起来的不同主机上的应用程序之间),也就是IPC或RPC的概念。

现代操作系统至少支持下列 domain:

  • UNIX(AF_UNIX) domain :允许在同一主机上的应用程序之间进行通信。
  • IPv4(AF INET) domain :允许在使用因特网协议 IP4 网络连接起来的主机上的应用程序之间进行通信。
  • IPV6 (AF INET6) domain :允许在使用因特网协议 IPV6 网络连接起来的主机上的应用程序之间进行通信。
Domaindomain执行的通信应用程序间的通信地址格式地址结构
Unix DomainAF_UNIX内核中同一主机路径名sockaddr un
Internet DomainAF_INET通过IPv4通过 IPv4 网络连接起来的主机32位IPV4 地址+16 位端口号sockaddrin
Internet DomainAF_INET6通过IPv6通过 IPv6 网络连按起来的主机128 位 IPv6 地址+16 位端口号sockaddr in6

1.2 socket 类型

  每个 socket 实现都至少提供了两种 socket:流和数据报。这两种 socket 类型在 Unix domain 和 Internet domain 中都得到了支持。下表对这两种 socket 类型的属性进行了总结:

属性socket 类型
流 (SOCK_STREAM)数据报 (SOCK_DGRAM)
可靠传输?YesNo
消息边界保留?NoYes
面向链接?YesNo

  流 socket (SOCK_STREAM) 提供了一个可靠的双向的字节流通信信道。在这段描述中的术语的含义如下:

  • 可靠的:表示可以保证发送者传输的数据会完整无缺地到达接收应用程序 (假设网络链接和接收者都不会崩溃) 或收到一个传输失败的通知。
  • 双向的:表示数据可以在两个 socket 之间的任意方向上传输。
  • 字节流:表示与管道一样不存在消息边界的概念 。

  一个流 socket 类似于使用一对允许在两个应用程序之间进行双向通信的管道,它们之间的差别在于 (Internet domain) socket 允许在网络上进行通信。
  流 socket 的正常工作需要一对相互连接的 socket,因此流 socket 通常被称为面向连接的。术语“对等 socket”是指连接另一端的 socket,“对等地址”表示该 socket 的地址,“对等应用程序”表示利用这个对等 socket 的应用程序。有些时候,术语“远程”(或外部) 是作为对等的同义词使用。类似地,有些时候术语“本地”被用来指连接的这一端上的应用程序、socket或地址。一个流 socket 只能与一个对等 socket 进行连接。

  数据报 socket (SOCK_DGRAM)允许数据以被称为数据报的消息的形式进行交换。在数据报 socket 中,消息边界得到了保留,但数据传输是不可靠的。消息的到达可能是无序的、重复的或者根本就无法到达。
  数据报 socket 是更一般的无连接 socket 概念的一个示例。与流 socket 不同,一个数据报socket 在使用时无需与另一个 socket 连接。
在 Internet domain 中,数据报 socket 使用了用户数据报协议 (UDP),而流 socket 则 (通常)使用了传输控制协议 (TCP)。一般来讲,在称呼这两种 socket 时不会使用术语“Internet domain数据报 socket” 和 “Internet domain 流 socket”,而是分别使用术语“UDP socket”和“TCP socket”。

1.3 Socket 相关的系统调用

关键的 socket 系统调用包括以下几种:

  • socket() 系统调用创建一个新 socket。
  • bind() 系统调用将一个 socket 绑定到一个地址上。通常,服务器需要使用这个调用来将其 socket 绑定到一个众所周知的地址上使得客户端能够定位到该 socket 上。
  • listen() 系统调用允许一个流 socket 接受来自其他 socket 的接入连接。
  • accept() 系统调用在一个监听流 socket 上接受来自一个对等应用程序的连接,并可选地返回对等 socket 的地址。
  • connect() 系统调用建立与另一个 socket 之间的连接。

2 Internet DOMAIN的C/S模型

2.1 TCP socket 的C/S模型(SOCK_STREAM)

在这里插入图片描述
1. 建立连接过程
  服务器调用socket()、bind()、listen() 完成初始化后,调用 accept() 阻塞等待,处于监听端口的状态。客户端调用 socket() 初始化后,调用 connect() 发出 SYN 段并阻塞等待服务器应答,服务器应答一个 SYN-ACK 段,客户端收到后从 connect() 返回,同时应答一个 ACK 段,服务器收到后从 accept() 返回。

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

3. 断开连接过程
  若客户端没有更多的请求了,就调用 close() 关闭连接,就像写端关闭的管道一样,服务器的 read() 返回 0,这样服务器就知道客户端关闭了连接,也调用 close() 关闭连接。

注意:任何一方调用close() 后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用 shutdown() 则连接处于半关闭状态,仍可接收对方发来的数据。

2.1.1 Server端代码

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>#include "wrap.h"#define SERV_PORT 6666int main(void)
{int sfd, cfd;int len, i;char buf[BUFSIZ], clie_IP[BUFSIZ];struct sockaddr_in serv_addr, clie_addr;socklen_t clie_addr_len;sfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&serv_addr, sizeof(serv_addr));           serv_addr.sin_family = AF_INET;                 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  serv_addr.sin_port = htons(SERV_PORT);          Bind(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));Listen(sfd, 2);                                printf("wait for client connect ...\n");clie_addr_len = sizeof(clie_addr_len);cfd = Accept(sfd, (struct sockaddr *)&clie_addr, &clie_addr_len);printf("cfd = ----%d\n", cfd);printf("client IP: %s  port:%d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof(clie_IP)), ntohs(clie_addr.sin_port));while (1) {len = Read(cfd, buf, sizeof(buf));Write(STDOUT_FILENO, buf, len);for (i = 0; i < len; i++)buf[i] = toupper(buf[i]);Write(cfd, buf, len); }Close(sfd);Close(cfd);return 0;
}

2.1.2 Client端代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>#include "wrap.h"#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666int main(void)
{int sfd, len;struct sockaddr_in serv_addr;char buf[BUFSIZ]; sfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&serv_addr, sizeof(serv_addr));                       serv_addr.sin_family = AF_INET;                             inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);    serv_addr.sin_port = htons(SERV_PORT);                      Connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));while (1) {fgets(buf, sizeof(buf), stdin);int r = Write(sfd, buf, strlen(buf));       printf("Write r ======== %d\n", r);len = Read(sfd, buf, sizeof(buf));printf("Read len ========= %d\n", len);Write(STDOUT_FILENO, buf, len);}Close(sfd);return 0;
}

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

2.2 UDP socket 的C/S模型(SOCK_DGRAM)

在这里插入图片描述
  由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现

2.2.1 Server端代码

#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>#define SERV_PORT 8000int main(void)
{struct sockaddr_in serv_addr, clie_addr;socklen_t clie_addr_len;int sockfd;char buf[BUFSIZ];char str[INET_ADDRSTRLEN];int i, n;sockfd = socket(AF_INET, SOCK_DGRAM, 0);bzero(&serv_addr, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(SERV_PORT);bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));printf("Accepting connections ...\n");while (1) {clie_addr_len = sizeof(clie_addr);n = recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)&clie_addr, &clie_addr_len);if (n == -1)perror("recvfrom error");printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),ntohs(clie_addr.sin_port));for (i = 0; i < n; i++)buf[i] = toupper(buf[i]);n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr));if (n == -1)perror("sendto error");}close(sockfd);return 0;
}

2.2.2 Client端代码

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>#define SERV_PORT 8000int main(int argc, char *argv[])
{struct sockaddr_in servaddr;int sockfd, n;char buf[BUFSIZ];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, BUFSIZ, stdin) != NULL) {n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));if (n == -1)perror("sendto error");n = recvfrom(sockfd, buf, BUFSIZ, 0, NULL, 0);         //NULL:不关心对端信息if (n == -1)perror("recvfrom error");write(STDOUT_FILENO, buf, n);}close(sockfd);return 0;
}

2.3 TCP socket 与 UDP socket 的区别

  Internet DOMAIN中的传输层主要应用的协议模型:TCP协议和UDP协议。TCP协议在网络通信中占主导地位,绝大多数的网络通信借助TCP协议完成数据传输。但UDP也是网络通信中不可或缺的重要通信手段。
  相较于TCP而言,UDP通信的形式更像是发短信。不需要在数据传输之前建立、维护连接。只专心获取数据就好。省去了三次握手的过程,通信速度可以大大提高,但与之伴随的通信的稳定性和正确率便得不到保证。因此,称UDP为“无连接的不可靠报文传递”。
  与TCP相比,UDP有哪些优点和不足呢?由于无需创建连接,所以UDP开销较小,数据传输速度快,实时性较强。多用于对实时性要求较高的通信场合,如视频会议、电话会议等。但随之也伴随着数据传输不可靠,传输数据的正确率、传输顺序和流量都得不到控制和保证。所以,通常情况下,使用UDP协议进行数据传输,为保证数据的正确性,需要在应用层添加辅助校验协议来弥补UDP的不足,以达到数据可靠传输的目的
  与TCP类似的,UDP也有可能出现缓冲区被填满后,再接收数据时丢包的现象。由于它没有TCP滑动窗口的机制,通常采用如下两种方法解决:

  1. 服务器应用层设计流量控制,控制发送数据速度。
  2. 借助 setsockopt 函数改变接收缓冲区大小。如:
    #include <sys/socket.h>
    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);int n = 220x1024
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));
    

3 Unix DOMAIN的C/S模型

  socket API 原本是为网络通讯设计的,后来在 socket 的框架上发展出一种 IPC 机制,也就是Unix Domain Socket。虽然网络 socket 也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是Unix Domain Socket 用于 IPC 更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。Unix Domain Socket 也提供面向流和面向数据报两种 API 接口,类似于 TCP 和 UDP,但是面向消息的 Unix Domain Socket 也是可靠的,消息既不会丢失也不会顺序错乱
在这里插入图片描述
  UNIX Domain Socket是全双工的,API接口语义丰富,相比其它 IPC 机制有明显的优越性,目前已成为使用最广泛的IPC机制。使用Unix Domain Socket的过程和网络 socket 相似,也要先调用 socket() 创建一个 socket 文件描述符,address_family 指定为 AF_UNIX,type 可以选择 SOCK_DGRAM/SOCK_STREAM,protocol 参数仍然指定为 0。
  Unix Domain Socket 与网络 socket 编程最明显的不同在于地址格式不同,用结构体 sockaddr_un 表示,网络编程的 socket 地址是IP地址+端口号,而 Unix Domain Socket 的地址是一个 socket 类型的文件在文件系统中的路径此 socket 文件由 bind() 调用创建,如果调用 bind() 时该文件已存在,则 bind() 错误返回

对比网络套接字地址结构和本地套接字地址结构:

struct sockaddr_in {
__kernel_sa_family_t sin_family; 			/* Address family */  	地址结构类型
__be16 sin_port;					 	/* Port number */		端口号
struct in_addr sin_addr;					/* Internet address */	IP地址
};struct sockaddr_un {
__kernel_sa_family_t sun_family; 		/* AF_UNIX */			地址结构类型
char sun_path[UNIX_PATH_MAX]; 		/* pathname */		socket文件名(含路径)
};

3.1 C/S数据模型(SOCK_STREAM)

3.1.1 Server端代码

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>#include "wrap.h"#define SERV_ADDR  "serv.socket"int main(void)
{int lfd, cfd, len, size, i;struct sockaddr_un servaddr, cliaddr;char buf[4096];lfd = Socket(AF_UNIX, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sun_family = AF_UNIX;strcpy(servaddr.sun_path,SERV_ADDR);len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path);     /* servaddr total len */unlink(SERV_ADDR);                              /* 确保bind之前serv.sock文件不存在,bind会创建该文件 */Bind(lfd, (struct sockaddr *)&servaddr, len);           /* 参3不能是sizeof(servaddr) */Listen(lfd, 20);printf("Accept ...\n");while (1) {len = sizeof(cliaddr);cfd = Accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&len);len -= offsetof(struct sockaddr_un, sun_path);      /* 得到文件名的长度 */cliaddr.sun_path[len] = '\0';                       /* 确保打印时,没有乱码出现 */printf("client bind filename %s\n", cliaddr.sun_path);while ((size = read(cfd, buf, sizeof(buf))) > 0) {for (i = 0; i < size; i++)buf[i] = toupper(buf[i]);write(cfd, buf, size);}close(cfd);}close(lfd);return 0;
}

3.1.2 Client端代码

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>#include "wrap.h"#define SERV_ADDR "serv.socket"
#define CLIE_ADDR "clie.socket"int main(void)
{int  cfd, len;struct sockaddr_un servaddr, cliaddr;char buf[4096];cfd = Socket(AF_UNIX, SOCK_STREAM, 0);bzero(&cliaddr, sizeof(cliaddr));cliaddr.sun_family = AF_UNIX;strcpy(cliaddr.sun_path,CLIE_ADDR);len = offsetof(struct sockaddr_un, sun_path) + strlen(cliaddr.sun_path);     /* 计算客户端地址结构有效长度 */unlink(CLIE_ADDR);Bind(cfd, (struct sockaddr *)&cliaddr, len);                                 /* 客户端也需要bind, 不能依赖自动绑定*/bzero(&servaddr, sizeof(servaddr));                                          /* 构造server 地址 */servaddr.sun_family = AF_UNIX;strcpy(servaddr.sun_path,SERV_ADDR);len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path);   /* 计算服务器端地址结构有效长度 */Connect(cfd, (struct sockaddr *)&servaddr, len);while (fgets(buf, sizeof(buf), stdin) != NULL) {write(cfd, buf, strlen(buf));len = read(cfd, buf, sizeof(buf));write(STDOUT_FILENO, buf, len);}close(cfd);return 0;
}

3.2 C/S数据模型(SOCK_DGRAM)

  因为Unix Domain Socket是用于IPC的,所以知道SOCK_STREAM类型的就够了,这里暂不做例子的代码编写了。


http://www.ppmy.cn/news/88398.html

相关文章

Java的ssm框架中开发常用注解的作用和功能小小总结!!!

Java 的 SSM (Spring SpringMVC MyBatis) 框架是 Java Web 开发中常用的框架之一。其中&#xff0c;Spring、SpringMVC、MyBatis 框架各自都提供了很多注解&#xff0c;以下是一些常用注解及其功能&#xff1a; Spring 框架常用注解 Component&#xff1a;用于标记一个类为组…

视频会议产品对比分析

内网视频会议系统如何选择&#xff1f;有很多单位为了保密&#xff0c;只能使用内部网络&#xff0c;无法连接互联网&#xff0c;那些SaaS视频会议就无法使用。在内网的优秀视频会议也有很多可供选择&#xff0c;以下是几个常用的&#xff1a; 1. 宝利通&#xff1a;它支持多种…

2019下半年上午题

2019下半年上午题 b 选a c 最后统一单位 计算需要多少片芯片&#xff1a; 流水线&#xff1a; 也就是&#xff1a; 对于这一道题&#xff1a; c ssl&#xff1a;安全套接层 https&#xff1a;安全通道 PGP&#xff1a;电子邮件加密 d b a b b 受委托方和委…

Java枚举

Java枚举 &#x1f37a;1 背景及定义&#x1f37a;&#x1f9c3;2 使用&#x1f9c3;&#x1f964;3 枚举优点缺点&#x1f964;&#x1f375;4 枚举和反射&#x1f375;&#x1f377;4.1 枚举是否可以通过反射&#xff0c;拿到实例对象呢&#xff1f;&#x1f377; ☕️5 总结…

多文件分布式上传-SpringBoot

前言 在现代化的互联网应用中&#xff0c;各种形式的上传都成为了必备的功能之一。而对于大文件上传以及多文件上传来说&#xff0c;我们往往需要考虑分布式储存的方案&#xff0c;以实现高效和可扩展性。 本文将详细介绍在SpringBoot中实现多文件分布式上传的方法&#xff0…

Python多线程爬虫又来了

Python多线程的主要好处是可以在单个程序中同时执行多个任务&#xff0c;从而提高应用程序的性能和效率。具体来说&#xff0c;多线程有以下几个优点&#xff1a; 提高CPU利用率&#xff1a;通过多线程&#xff0c;可以更充分地利用CPU资源&#xff0c;尤其适用于计算密集型的…

项目中遇到的一些问题总结(十二)

有状态认证和无状态认证 有状态认证和无状态认证是两种不同的身份验证方式&#xff0c;主要的区别在于是否需要在服务端保留用户的会话或状态信息。 有状态认证&#xff1a;在服务器端记录与用户身份验证相关的会话信息&#xff0c;服务器需要维持这些会话信息来验证用户的每…

python中字符串的类型转换

一、使用eval----含有{}字符串的转换为list、tuple、dict- eval&#xff08;&#xff09;&#xff1a;将字符串str当成有效的表达式来求值并返回计算结果 &#xff08;1&#xff09;字符串转换为列表 &#xff08;2&#xff09;字符串转换为元组 a " ([1,3],[1,2],[1,1])…