Android 进阶:进程通信之 Socket (顺便回顾 TCP UDP)

7,214 阅读12分钟
  • 不要害怕困难,这是你进步的机会!

读完本文你将了解:

前面几篇文章我们介绍了 AIDLBinderMessenger 以及 ContentProvider 实现进程通信的方式,这篇文章将介绍“使用 Socket 进行跨进程通信”。

在介绍 Socket 之前我们先来回顾一下网络基础知识,有的知识需要经常回顾一下加深印象。

OSI 七层网络模型

为了使不同厂家生产的计算机可以相互通信,建立更大范围的计算机网络,国际标准化组织(ISO)在 1978 年提出了“开放系统互联参考模型”,即 OSI/RM 模型(Open System Interconnection/Reference Model)。

OSI 模型将计算机网络体系结构的通信协议划分为七层,每一层都建立在它的下层之上,同时向它的上一层提供一定服务。上层只管调用下层提供的服务,而不用关心具体实现细节,有些类似我们开发中对外暴露接口隐藏实现的思想。

七层模型自下而上分别为:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。其中低四层完成数据传输,高三层面向用户。

各层的功能见下图(图片来自 维基百科):

shixinzhang

TCP/IP 四层模型

由于 OSI/RM 模型过于复杂难以实现,现实中广泛使用的是 TCP/IP 模型。

TCP/IP 是一个协议集,是由 ARPA ( Advanced Research Projects Agency Network 高等研究计划署网络 ) 于 1977 到 1979 年推出的一种网络体系结构和协议规范。

随着 Internet 的发展,TCP/IP 得到进一步的研究和推广,成为 Internet 上的 “通用模型”。

TCP/IP 模型在 OSI 模型的基础上进行了简化,变成了四层,从下到上分别为:网络接口层、网络层、传输层、应用层。与 OSI 体系结构对比如下:

这里写图片描述

可以看到,TCP/IP 模型 的网络接口层对应 OSI 模型的物理层、数据链路层,应用层对应会话层、表示层和应用层每一层的功能如下:

  • 应用层:应用程序为了访问网络所使用的一层
    • 数据以应用内部使用的格式进行传送,然后被编码成标准协议的格式
    • 比如万维网使用的 HTTP 协议,传输文件的 FTP 协议等等
  • 传输层:响应来自应用层的请求,并向网络层发出服务请求
    • 提供两台主机之间的数据传输,通常用于端到端连接、流量控制或者错误恢复
    • 最重要的两个协议就是 TCP 和 UDP
  • 网络层:提供端到端的数据包交付
    • 负责数据包从源发送到目的地
    • 任务包括网络路由、差错控制和 IP 编制等
    • 重要的协议有 IP、ICMP 等
  • 网络接口层:负责通过网络发送和接受 IP 数据包

每一层包括的协议如下图:

这里写图片描述

Socket 作为应用层和传输层之间的桥梁,与之关系最大的两个协议就是传输层中的 TCP 和 UDP协议。

这里写图片描述
(图片来自: www.jianshu.com/p/089fb79e3…

Socket 分为流式套接字和用户数据报套接字,分别使用传输层中的 TCP 和 UDP 协议。

TCP 协议

TCP (Transmission Control Protocol 传输控制协议),是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP 协议被认为是稳定的协议,因为它有以下特点:

  • 面向连接,“三次握手”
  • 双向通信
  • 保证数据按序发送,按序到达
  • 超时重传

要使用 TCP 传输数据,必须先建立连接,传输完成后释放连接。分别对应常说的“三次握手”、“四次挥手”。

TCP 的三次握手

在 socket 编程中,客户端执行 connect() 时。将触发三次握手。

TCP 的三次握手流程图如下:

这里写图片描述

解释如下:

  1. 客户端发送一个建立 C 到 S 连接的请求报文,其中同步标志位(SYN)置 1。然后进入 SYN_SEND 状态,等待服务端确认
  2. 服务端返回确认数据报文,将 ACK 置为 1,同时也将 SYN 置为 1,请求建立 S 到 C 的连接
  3. 客户端返回确认数据报文,ACK 递增,这时双方连接建立成功

双向连接都建立成功后就可以收发数据了。

为什么是三次呢?

为了防止已经失效的连接请求报文突然又传送到服务端,因而产生错误。 减小因延迟高拥塞大对报文传输的影响。

在这三次握手过程中,任何一次未收到对面回复都要重发,保证请求报文的及时性。

建立连接是需要耗费资源的,就像打电话一样,只有在双方都确认后才等待通话,只要有一方没有及时响应就挂断,而不是一方确认后就等着,这样会浪费资源,甚至可能导致其他问题。

一副图简化理解三次握手:

这里写图片描述

TCP 的四次挥手

TCP 协议中,在通信结束后,需要断开连接,这需要通过四次挥手,客户端或服务器均可主动发起,主动的一方先断开

在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

流程图如下 (图片来自:blog.csdn.net/sszgg2006/a…):

这里写图片描述

解释如下:

  1. 客户端 C 发送 FIN 的报文,表示没有数据要发送给服务端了,请求关闭 C 到 S 的连接
  2. 服务端确认这个报文,发回一个 ACK,关闭它的 Receive 通道;客户端收到 ACK 后关闭它的 Send 通道
  3. 服务端 S 发出 FIN ,表示没有数据发送给客户端了,请求断开连接
  4. 客户端确认这个报文,发回 ACK,等待 2MSL 后关闭 Receive 通道;S 收到后关闭 Send 通道

注意第三步,S 发出 FIN 后还没有断开!

为什么是四次呢?

TCP 连接是全双工的,每一端都可以同时发送和接受数据,关闭的时候两端都要关闭各自两个方向的通道,总共相当于要关闭四个。

(假设以客户端先发起断开请求)

  • 在客户端发送 FIN 报文时,仅代表客户端没有数据发送了
  • 这时服务端可能还是有数据要发送,因此不会马上关闭服务端到客户端的发送通道,而是先回答 ACK “哦知道了,我先不接收你的数据,你先断了发送通道吧”;客户端收到服务端的确认消息后,断开到服务端的发送通道
  • 等服务端没有数据发送时,向客户端发送 FIN 报文,说“我没啥发的了,请求断开”
  • 客户端收到后回复 “好的你断吧”,同时断开到服务端的接受通道;服务端得到确认后断开到客户端的发送通道

至此,四个通道全部关闭。

第四步客户端为什么要等待 2MSL?

首先,MSL(Maximum Segment Life),是 TCP 对 TCP Segment 生存时间的限制。

客户端在发出确认服务端关闭的 ACK 后,它没有办法知道对方是否收到这个消息,于是需要等待一段时间,如果服务端没有收到关闭的消息后会重新发出 FIN 报文,这样客户端就知道自己上条消息丢了,需要再发一次;如果等待的这段时间没有在收到 FIN 的重发报文,说明它的确已经收到断开的消息并且已经断开了。

这个等待时间至少是:客户端的 timeout + FIN 的传输时间,为了保证可靠,采用更加保守的等待时间 2MSL。

UDP 协议

UDP 协议没有 TCP 协议稳定,因为它不建立连接,也不按顺序发送,可能会出现丢包现象,使传输的数据出错。

但是有得就有失,UDP 的效率更高,因为 UDP 头包含很少的字节,比 TCP 负载消耗少,同时也可以实现双向通信,不管消息送达的准确率,只负责无脑发送。

UDP 服务于很多知名应用层协议,比如 NFS(网络文件系统)、SNMP(简单网络管理协议)

UDP 一般多用于 IP 电话、网络视频等容错率强的场景。

Socket 简介

TCP 或者 UDP 的报文中,除了数据本身还包含了包的信息,比如目的地址和端口,包的源地址和端口,以及其他附加校验信息。

由于包的长度有限,在传输的过程中还需要拆包,到达目的地后再重新组合。

如果有丢失或者损坏的包还需要重传,有的在到达目的地后还需要重新排序。

这些工作是复杂且与业务无关的,Socket 为我们封装了这些处理工作。

Socket 被称为“套接字”,它把复杂的 TCP/IP 协议簇隐藏在背后,为用户提供简单的客户端到服务端接口,让我们感觉这边输入数据,那边就直接收到了数据,像一个“管道”一样。

这里写图片描述

Socket 的基本操作

Socket 的基本操作有以下几部分:

  1. 连接远程机器
  2. 发送数据
  3. 接收数据
  4. 关闭连接
  5. 绑定端口
  6. 监听到达数据
  7. 在绑定的端口上接受来自远程机器的连接

要实现客户端与服务端的通信,双方都需要实例化一个 Socket。

Java 中,客户端可以实现上面的 1、2、3、4、,服务端实现 5、6、7.

Java.NET 中为我们提供了使用 TCP、UDP 通信的两种 Socket:

  • ServerSocket:流套接字,TCP
  • DatagramSocket:数据报套接字,UDP

使用 TCP 通信的 Socket 流程

服务端:

  1. 调用 ServerSocket(int port) 创建一个 ServerSocket,绑定到指定端口
  2. 调用 accept() 监听连接请求,如果客户端请求连接则接受,返回通信套接字
  3. 调用 Socket 类的 getOutputStream()getInputStream() 获取输出和输入流,进行网络数据的收发
  4. 关闭套接字

客户端:

  1. 调用 Socket() 创建一个流套接字,连接到服务端
  2. 调用 Socket 类的 getOutputStream()getInputStream() 获取输出和输入流,进行网络数据的收发
  3. 关闭套接字

使用 UDP 通信的 Socket 流程

服务端:

  1. 调用 DatagramSocket(int port) 创建一个数据报套接字,绑定到指定端口
  2. 调用 DatagramPacket(byte[] buf, int length) 建立一个字节数组,以接受 UDP 包
  3. 调用 DatagramSocketreceive() 接收 UDP 包
  4. 关闭数据报套接字

客户端:

  1. 调用 DatagramSocket() 创建一个数据报套接字
  2. 调用 DatagramPacket(byte buf[], int offset, int length,InetAddress address, int port) 建立要发送的 UDP 包
  3. 调用 DatagramSocket.send() 发送 UDP 包
  4. 关闭数据报套接字

使用 TCP 通信的 Socket 实现跨进程聊天

我们使用流套接字实现一个跨进程聊天程序。

创建服务端 TCPServerService

public class TCPServerService extends BaseService {

    private final String TAG = this.getClass().getSimpleName();

    private boolean mIsServiceDisconnected;

    @Override
    public void onCreate() {
        super.onCreate();
        LogUtils.d(TAG, "服务已 create");
        new Thread(new TCPServer()).start();    //新开一个线程开启 Socket
    }


    private class TCPServer implements Runnable {
        @Override
        public void run() {
            ServerSocket serverSocket;
            try {
                serverSocket = new ServerSocket(ConfigHelper.TEST_SOCKET_PORT);
                LogUtils.d(TAG, "TCP 服务已创建");
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("TCP 服务端创建失败");
                return;
            }

            while (!mIsServiceDisconnected) {
                try {
                    Socket client = serverSocket.accept();  //接受客户端消息,阻塞直到收到消息
                    //我这里使用了线程池,也可以直接新建一个线程
//                    new Thread(responseClient(client)).start();    
                    ThreadPoolManager.getInstance()
                            .addTask(responseClient(client));    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //在这里接受和回复客户端消息
    private Runnable responseClient(final Socket client) {
        return new Runnable() {
            @Override
            public void run() {
                try {
                    //接受消息
                    BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
                    //回复消息
                    PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())), true);
                    out.println("服务端已连接 *****");

                    while (!mIsServiceDisconnected) {
                        String inputStr = in.readLine();
                        LogUtils.i(TAG, "收到客户端的消息:" + inputStr);
                        if (TextUtils.isEmpty(inputStr)) {
                            LogUtils.i(TAG, "收到消息为空,客户端断开连接 ***");
                            break;
                        }
                        out.println("你这句【" + inputStr + "】非常有道理啊!");
                    }
                    out.close();
                    in.close();
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };
    }

    @Override
    public void onDestroy() {
        mIsServiceDisconnected = true;
        super.onDestroy();
    }
}

然后在 AndroidManifest.xml 文件中声明 Service,放到另外一个进程:

<service
    android:name=".service.TCPServerService"
    android:exported="true"
    android:process=":socket"/>

在客户端中建立连接,收发数据

这里的客户端就是我们的 Activity。

布局代码:

<merge xmlns:tools="http://schemas.android.com/tools"
       xmlns:android="http://schemas.android.com/apk/res/android">


    <TextView
        android:id="@+id/tv_socket_message"
        android:layout_width="match_parent"
        android:layout_height="300dp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginBottom="50dp"
        android:background="#efefef"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/et_client_socket"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            />

        <Button
            android:id="@+id/bt_send_socket"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:enabled="false"
            android:text="向服务器发消息"/>
    </LinearLayout>
</merge>

然后 include 到 Activity 的布局文件中。

Activity 代码:


/**
 * 处理 Socket 线程切换
 */
@SuppressWarnings("HandlerLeak")
public class SocketHandler extends Handler {
    public static final int CODE_SOCKET_CONNECT = 1;
    public static final int CODE_SOCKET_MSG = 2;

    @Override
    public void handleMessage(final Message msg) {
        switch (msg.what) {
            case CODE_SOCKET_CONNECT:
                mBtSendSocket.setEnabled(true);
                break;
            case CODE_SOCKET_MSG:
                mTvSocketMessage.setText(mTvSocketMessage.getText() + (String) msg.obj);
                break;
        }
    }
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_ipc);
    ButterKnife.bind(this);
    bindSocketService();
}

private void bindSocketService() {
    //启动服务端
    Intent intent = new Intent(this, TCPServerService.class);
    startService(intent);

    mSocketHandler = new SocketHandler();
    new Thread(new Runnable() {    //新开一个线程连接、接收数据
        @Override
        public void run() {
            try {
                connectSocketServer();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

private Socket mClientSocket;
private PrintWriter mPrintWriter;
private SocketHandler mSocketHandler;

/**
 * 通过 Socket 连接服务端
 */
private void connectSocketServer() throws IOException {
    Socket socket = null;
    while (socket == null) {    //选择在循环中连接是因为有时请求连接时服务端还没创建,需要重试
        try {
            socket = new Socket("localhost", ConfigHelper.TEST_SOCKET_PORT);
            mClientSocket = socket;
            mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
        } catch (IOException e) {
            SystemClock.sleep(1_000);
        }
    }

    //连接成功
    mSocketHandler.sendEmptyMessage(SocketHandler.CODE_SOCKET_CONNECT);

    //获取输入流
    BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    while (!isFinishing()) {    //死循环监听服务端发送的数据
        final String msg = in.readLine();    
        if (!TextUtils.isEmpty(msg)) {
            //数据传到 Handler 中展示
            mSocketHandler.obtainMessage(SocketHandler.CODE_SOCKET_MSG,
                    "\n" + DateUtils.getCurrentTime() + "\nserver : " + msg)
                    .sendToTarget();
        }
        SystemClock.sleep(1_000);
    }

    System.out.println("Client quit....");
    mPrintWriter.close();
    in.close();
    socket.close();
}

@OnClick(R.id.bt_send_socket)
public void sendMsgToSocketServer() {
    final String msg = mEtClientSocket.getText().toString();
    if (!TextUtils.isEmpty(msg) && mPrintWriter != null) {
        //发送数据,这里注意要在线程中发送,不能在主线程进行网络请求,不然就会报错
        ThreadPoolManager.getInstance().addTask(new Runnable() {
            @Override
            public void run() {
                mPrintWriter.println(msg);
            }
        });
        mEtClientSocket.setText("");
        mTvSocketMessage.setText(mTvSocketMessage.getText() + "\n" + DateUtils.getCurrentTime() + "\nclient : " + msg);
    }
}

运行结果

这里写图片描述

代码地址

Thanks

《计算机网络》
Android 开发艺术探索》
《深入理解 Android 网络编程》
www.cnblogs.com/BlueTzar/ar…
blog.csdn.net/mad1989/art…
www.jianshu.com/p/089fb79e3…
blog.csdn.net/sszgg2006/a…
www.zhihu.com/question/36…