综述

  在前面的几篇文章中,我们介绍了许多在Android中有关进程间通信的方式,但都是在一个设备上进行的进程间通信,而这时候我们两个应用在不同的设备上的时候,在这个时候我们就不能通过前方介绍的那些方法来解决了。但是我们通过网络进行通信来处理这个问题。今天就来介绍一下Android中网络通信的其中一种方式——Socket。Socket翻译为中文为套接字,而现在套接字也成为了操作系统中的一部分。下面我们就来看一下如何使用这个套接字的。

TCP/IP介绍

  这里有一点需要说明一下,在Internet所使用的各种协议中,最重要的和最著名的就是TCP和IP两个协议。而我们现在经常提到的TCP/IP并不一定单指TCP和IP这两个具体的协议,而往往表示Internet所使用的整个TCP/IP协议族。

TCP/IP的体系结构

  TCP/IP协议可以为各式各样的应用提供服务,同时TCP/IP协议也允许IP协议在各式各样的网络构成的互联网上运行。TCP/IP协议是一个四层的体系结构,它包含应用层、传输层、网络层、网络接口层。在传输层中主要使用的有两种协议:TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议)。下面是TCP/IP体系结构示意图。

android tcp通讯权限 android tcpip_android

传输控制协议TCP

  TCP是TCP/IP体系中非常复杂的一个协议,在这里我们简单看一下TCP协议的特点,对于TCP协议的详细内容,可以查看一些计算机网络的相关书籍。
1. TCP是面向连接传输协议,也就是说在我们的应用程序在使用TCP协议之前,必须先建立起TCP连接。在传送数据完毕后,必须释放已经建立的TCP连接。就像我们打电话一样,打电话之前首先需要拨号进行建立连接,等通话结束后再挂断释放连接。
2. 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的。
3. TCP提供了一个可靠交付的服务,也就是说通过TCP连接传送的数据,无差错,不丢失,不重复,并且按序到达。
4. TCP提供全双工通信,它允许通信双方的应用进程在任何时候都能够发送数据。
5. TCP通信中是面向字节流的,其中的“流”指的是流入到进程或从进程流出的字节序列。

用户数据报协议UDP

  用户数据报协议UDP只是在IP的数据服务上增加了很少的一点功能(复用、分用的功能以及差错检测的功能)。在这里简单说一下UDP的特点。
1. UDP是无连接,也就是在发送数据之前是不需要建立连接的,也就减少了开销和发送数据之间的延时。
2. UDP它只能是尽最大努力地交付,也就是不能够保证可靠交付。
3. UDP它是面向报文的。发送方的UDP对应用程序交下来的报文,再添加首部后就向下交付给IP层。
4. UDP它没有拥塞控制,也就是说在网络出现拥塞的情况下不会使源主机的发送速率降低。
5. UDP支持一对一,一对多,多对一和多对多的交互通信。

Socket在TCP/IP中的作用

  Socket是工作于TCP/IP协议中应用层和传输层之间的一种抽象(不属于应用层也不属于传输层)。在Android系统中,它可以分为流套接字(streamsocket)和数据报套接字(datagramsocket)。而Socket中的流套接字将TCP协议作为其端对端协议,提供了一个可信赖的字节流服务;数据报套接字使用UDP协议,提供数据打包发送服务。

  在网络编程的时候,我们经常把Socket作为应用进程和传输层协议之间的接口。在下面图中表示了这样一个概念。在图中我们假定了运输层使用的是TCP协议(如果使用的是UDP协议,情况也是类似的,只是UDP是无连接的通信的两端依然可以用两个套接字来标志)。并且现在套接字已经成为操作系统内核的一部分。

android tcp通讯权限 android tcpip_android_02


  不过有一点我们要注意,在套接字以上的进程是受应用程序控制的,而在套接字以下的的传输层协议软件则是由计算机操作系统控制。因此,只要我们的应用程序使用TCP/IP协议进行通信,它就必须通过Socket与操作系统交互并请求服务。从这里可以看出来,我们对Socket以上的应用进程具有完全的控制,但对Socket以下的传输层却只有很少的控制。例如,我们可以选择传输层协议(TCP或UDP)和一些传输层的参数(如最大缓存空间和最大报文长度)。

Socket使用案例

  在这里我们选择传输层协议为TCP协议,也就是我们将使用流套接字作为例子进行举例说明。现在我们现在做一个聊天室功能。在这里我们创建两个应用程序,分别运行在两个不同的设备上。首先看一下效果图。

演示



android tcp通讯权限 android tcpip_ipc_03

    

android tcp通讯权限 android tcpip_socket_04


客户端代码

package com.example.ljd.socketclient;

import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.sql.Date;
import java.text.SimpleDateFormat;

import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity{

    private static final int RECEIVE_NEW_MESSAGE = 1;
    private static final int SOCKET_CONNECT_SUCCESS = 2;
    private static final int SOCKET_CONNECT_FAIL = 3;

    @Bind(R.id.msg_edit_text)
    EditText mMessageEditText;

    @Bind(R.id.show_linear)
    LinearLayout mShowLinear;

    private PrintWriter mPrintWriter;
    private Socket mClientSocket;
    private boolean mIsConnectServer = false;

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {

                case RECEIVE_NEW_MESSAGE:
                    TextView textView = new TextView(MainActivity.this);
                    textView.setText((String)msg.obj);
                    mShowLinear.addView(textView);
                    break;

                case SOCKET_CONNECT_SUCCESS:
                    Toast.makeText(MainActivity.this,"连接服务端成功",Toast.LENGTH_SHORT).show();
                    break;

                case SOCKET_CONNECT_FAIL:
                    Toast.makeText(MainActivity.this,"连接服务端失败,请重新尝试",Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;

            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

    }

    @Override
    protected void onDestroy() {
        ButterKnife.unbind(this);
        disConnectServer();
        super.onDestroy();
    }

    @OnClick({R.id.send_btn,R.id.connect_btn,R.id.disconnect_btn})
    public void onClickButton(View v) {

        switch (v.getId()){
            case R.id.send_btn:
                sendMessageToServer();
                break;
            case R.id.connect_btn:
                new Thread() {
                    @Override
                    public void run() {
                        connectServer();
                    }
                }.start();
                break;
            case R.id.disconnect_btn:
                Toast.makeText(MainActivity.this,"已经断开连接",Toast.LENGTH_SHORT).show();
                disConnectServer();
                break;
        }

    }

    private String getTime(long time) {
        return new SimpleDateFormat("(HH:mm:ss)").format(new Date(time));
    }

    private void connectServer() {

        if (mIsConnectServer)
            return;

        int count = 0;
        while (mClientSocket == null) {
            try {
                mClientSocket = new Socket("10.10.14.160", 8088);
                mPrintWriter = new PrintWriter(new BufferedWriter(
                        new OutputStreamWriter(mClientSocket.getOutputStream())), true);
                mIsConnectServer = true;
                mHandler.obtainMessage(SOCKET_CONNECT_SUCCESS).sendToTarget();
            } catch (IOException e) {
                SystemClock.sleep(1000);
                count++;
                if (count == 5){
                    mHandler.obtainMessage(SOCKET_CONNECT_FAIL).sendToTarget();
                    return;
                }
            }
        }

        try {
            // 接收服务器端的消息
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
                    mClientSocket.getInputStream()));
            while (!MainActivity.this.isFinishing()) {
                String msg = bufferedReader.readLine();
                if (msg != null) {
                    String time = getTime(System.currentTimeMillis());
                    final String showedMsg = "server " + time + ":" + msg;
                    mHandler.obtainMessage(RECEIVE_NEW_MESSAGE, showedMsg)
                            .sendToTarget();
                }
            }
            mPrintWriter.close();
            bufferedReader.close();
            mClientSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void disConnectServer(){
        mIsConnectServer = false;
        if (mClientSocket != null) {
            try {
                mClientSocket.shutdownInput();
                mClientSocket.close();
                mClientSocket = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void sendMessageToServer(){
        if (!mIsConnectServer){
            Toast.makeText(this,"没有连接上服务端,请重新连接",Toast.LENGTH_SHORT).show();
            return;
        }
        final String msg = mMessageEditText.getText().toString();
        if (!TextUtils.isEmpty(msg) && mPrintWriter != null) {
            mPrintWriter.println(msg);
            mMessageEditText.setText("");
            String time = getTime(System.currentTimeMillis());
            final String showedMsg = "client " + time + ":" + msg;
            TextView textView = new TextView(this);
            textView.setText(showedMsg);
            mShowLinear.addView(textView);
        }
    }
}

客户端布局代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp"
    >

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" >

        <LinearLayout
            android:id="@+id/show_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >
        </LinearLayout>
    </ScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/msg_edit_text"
            android:layout_width="0dp"
            android:layout_gravity="bottom"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            />

        <Button
            android:id="@+id/send_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/connect_btn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="连接"/>

        <Button
            android:id="@+id/disconnect_btn"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="断开连接"/>
    </LinearLayout>
</LinearLayout>

服务端代码 

package com.example.ljd.socketserver;

import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.Date;
import java.text.SimpleDateFormat;


import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity{

    @Bind(R.id.show_linear)
    LinearLayout mShowLinear;

    @Bind(R.id.msg_edit_text)
    EditText mMessageEditText;

    private ServerSocket mServerSocket;
    private PrintWriter mPrintWriter;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 0){
                TextView textView = new TextView(MainActivity.this);
                textView.setText((String)msg.obj);
                mShowLinear.addView(textView);
            }
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        try {
            mServerSocket = new ServerSocket(8088);
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Thread(new AcceptClient()).start();
    }


    @Override
    public void onDestroy() {
        ButterKnife.unbind(this);
        if (mServerSocket != null){
            try {
                mServerSocket.close();
                mServerSocket = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        super.onDestroy();
    }

    @OnClick(R.id.send_btn)
    public void onClickButton() {
        final String msg = mMessageEditText.getText().toString();
        if (!TextUtils.isEmpty(msg) && mPrintWriter != null) {
            //将消息写入到流中
            mPrintWriter.println(msg);
            mMessageEditText.setText("");
            String time = getTime(System.currentTimeMillis());
            final String showedMsg = "server " + time + ":" + msg;
            TextView textView = new TextView(this);
            textView.setText(showedMsg);
            mShowLinear.addView(textView);
        }
    }

    private String getTime(long time) {
        return new SimpleDateFormat("(HH:mm:ss)").format(new Date(time));
    }

    class AcceptClient implements Runnable{

        @Override
        public void run() {
            try {
                Socket clientSocket = null;
                while (clientSocket == null){
                    clientSocket = mServerSocket.accept();
                    mPrintWriter = new PrintWriter(new BufferedWriter(
                            new OutputStreamWriter(clientSocket.getOutputStream())), true);
                }
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
                        clientSocket.getInputStream()));
                while (!MainActivity.this.isFinishing()) {
                    //读取客户端发来的消息
                    String msg = bufferedReader.readLine();
                    if (msg != null) {
                        String time = getTime(System.currentTimeMillis());
                        final String showedMsg = "client " + time + ":" + msg;
                        mHandler.obtainMessage(0, showedMsg)
                                .sendToTarget();
                    }
                }
                bufferedReader.close();
                clientSocket.close();
                mPrintWriter.close();

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端布局代码

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp"
    >

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" >

        <LinearLayout
            android:id="@+id/show_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >
        </LinearLayout>
    </ScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/msg_edit_text"
            android:layout_width="0dp"
            android:layout_gravity="bottom"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            />

        <Button
            android:id="@+id/send_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送" />
    </LinearLayout>

</LinearLayout>

添加权限

  在客户端与服务端应用中我们还需要添加一些权限

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

总结

  在网络通信中我们除了socket我们最常用的还有一种方式那就是http通信,而http连接采用的是”请求—响应方式“,也就是说只有当客户端发出请求时,服务端才能够向客户端返回数据。在这里我们就不在详细介绍。对于Android的IPC机制就说到这里了,当然还有其他方式可以进行跨进程通信,我们可以自行研究了。