Android进程间通信 - Socket使用(TCP、UDP)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hzw2017/article/details/81210979

在使用Socket实现进程间通信前,先对网络协议相关知识进行简单回顾。

网络分层

一般情况会将网络分为5层:

  • 应用层     常见协议:HTTP、FTP、POP3等
  • 传输层     常见协议:TCP、UDP
  • 网络层     常见协议:IP
  • 数据链路层
  • 物理层

这里写图片描述
这里写图片描述

TCP、UDP

  • TCP:面向连接的、可靠的流协议,提供可靠的通信传输。
    • 所谓流,就是指不间断的数据结构,你可以把它想象成排水管道中的水流。当应用程序采用TCP发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。
    • 有顺序控制、丢包重发机制
  • UDP:面向无连接的,具有不可靠性的数据报协议。(让广播和细节控制交给应用的通信传输)
    • 无顺序控制、丢包重发机制

TCP用于在传输层有必要实现可靠传输的情况,由于它是面向连接并具有“顺序控制”、重发控制等机制;而UDP则主要用于那些对高速传输和实时性有较高要求的通信或广播通信。
因此TCP和UDP应该根据应用的目的按需使用,没有绝对优缺点。

TCP三次握手与四次挥手

使用TCP协议的连接建立与断开,正常过程下至少需要发送7个包才能完成,就是我们常说的三次握手,四次挥手。
这里写图片描述

标志位Flags、序号

  • 序列号 Sequeuece number(seq): 数据包本身的序列号,初始序列号是随机的。
  • 确认号 Acknowledgment number(ack): 在接收端,用来通知发送端数据成功接收
  • 标志位,标志位只有为 1 的时候才有效
    • SYN(synchronize):表示在连接建立时用来同步序号。
    • ACK:TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1.
    • FIN(finish):用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
  • -

三次握手

三次握手:是指建立一个TCP连接时需要客户端和服务端总共发送3个包确认连接的建立。在Socket编程中,这一个过程由客户端执行connect来触发。

这里写图片描述

  1. 第一次握手:客户端向服务端发送请求报文;即SYN=1ACK=0,seq=x。
  2. 第二次握手:服务端收到客户端的请求报文,服务端会确认应答,告诉客户端已经收到请求了;即SYN=1,ACK=1,seq=y,ack=x+1;
  3. 第三次握手:客户端收到服务端的确认应答后,再次向服务端进行确认应答,建立完整的连接;即ACK=1,seq=x+1,ack=y+1

为什么要进行三次握手呢,或两次确认??
这里写图片描述
下面使用Wireshark抓包工具体验下三次握手的过程
这里写图片描述
红色框内就是一个TCP建立连接的过程

  1. 53324 —>80:嘿,哥们,我想访问你的web资源,能不能把你的80端口打开
  2. 80 —> 53324:可以啊,我已经把80端口打开了,为了保证我们的数据能可靠传输,你那边也需要把53324端口打开;
  3. 53324 —> 80:没问题,我已经把53324端口打开了,尽管的发送数据过来吧。

下面看看在三次握手的标志位的变化

1、53324 —>80
这里写图片描述
2、80 —> 53324
这里写图片描述
3、53324 —> 80
这里写图片描述

四次挥手

四次挥手:即终止TCP连接,就是断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在Socket编程中,这一工程由客户端或服务端任意一方执行close来触发。

扫描二维码关注公众号,回复: 4179062 查看本文章

由于 TCP 连接是全双工的,因此每个方向都必须单独进行关闭。这一原则是当一方数据发送完成,发送一个标志位为 FIN 的报文来终止这一方向的连接,收到标志位为 FIN 的报文意味着这一方向上不会再收到数据了。但是在 TCP 连接上仍然能够发送数据,直到这一方向也发送了 FIN 。发送 FIN 的一方执行主动关闭,另一方则执行被动关闭。

这里写图片描述

  1. 第一次挥手:客户端发送一个FIN=1,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说”我客户端没有数据要发给你了”,但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
  2. 第二次挥手:服务器端收到FIN后,先发送ack=u+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
  3. 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=1报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
  4. 第四次挥手:客户端收到FIN=1报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=w+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。

Socket

socket:

  • 位于传输层,是网络上的两个程序通过一个双向的通信连接 实现数据的交换的一种进程通信方式之一。
  • 成对出现,一对套接字
 Socket socket = new Socket("localhost", 8888);
 localhost:IP地址
 8888:端口号
即
IP地址 -- 端口号成对出现

socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供了网络开发所用的接口;HTTP是轿车,提供了封装或显示数据的具体形式;socket是发动机,提供了网络通信的能力。

典型的应用就是C/S结构:
这里写图片描述
从图可知,socket的使用是基于TCP或UDP协议。

Socket java简单实现

我们知道socket是基于TCPUDP协议实现的,下面以TCP协议为例实现,因为TCP更加常用些。

使用步骤

  1. 客户端
    1. 创建socket对象,指定成对的服务端IP地址和端口号
    2. 通过socket获取输出流,写入数据发给服务端
    3. 通过socket获取输入流,接受服务端的发送的数据
    4. 关闭资源close
  2. 服务端(与服务端类似)
    1. 创建ServerSocket对象,并指定端口号,其端口号必须与客服端一致
    2. 通过ServerSocket对象,获取客户端的socket实例(ServerSocket.accept方法
    3. 通过socket获取输入流,接受客户端发来的消息
    4. 通过socket获取输出流,写入数据向客户端发送数据作为回应
    5. 关闭资源close

具体实例

客户端Client

public class Client {

    private static final String TAG = "Client";

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                //IO操作不能放在主线程执行
                connectServer();
            }
        }).start();

    }

    private static void connectServer() {
        try {
            //1、创建客户端socket,指定服务端地址和端口
            Socket socket = new Socket("localhost", 8888);
            boolean connected = socket.isConnected(); //检查客户端与服务端是否连接成功
            System.out.println(connected?"连接成功":"连接失败,请重试!");

            //2、获取输出流,向服务器发送消息
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream),true);
            writer.write("第一次来到广州\n");
            writer.flush();

            //3、获取输入流,并读取服务端的响应信息
            InputStream inputStream = socket.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String info=reader.readLine();
            System.out.println("客户端收到服务端回应:"+info);


            //4、关闭资源
            outputStream.close();
            writer.close();
            inputStream.close();
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端Server

public class Server {

    private static final String TAG = "Server";

    public static void main(String[] args) {

        try {
            //1、创建ServerSocket对象,指定与客户端一样的端口号
            ServerSocket serverSocket = new ServerSocket(8888);
            //2、获取Socket实例
            final Socket socket = serverSocket.accept();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //3、获取输入流,接受客户端发来的消息
                        InputStream inputStream = socket.getInputStream();
                        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                        BufferedReader reader = new BufferedReader(inputStreamReader);
                        String info=reader.readLine();
                        System.out.println("服务端收到客户端的信息: " +info);

                        //4、获取输出流,向客户端发送消息回应
                        OutputStream outputStream = socket.getOutputStream();
                        PrintWriter writer = new PrintWriter(outputStream);
                        writer.write("羊城欢迎你!"+"\n");
                        writer.flush();

                        //4、关闭IO资源
                        inputStream.close();
                        reader.close();
                        outputStream.close();
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

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


}

注意:
使用write方法写入数据,字符串必须在中间或者末尾添加转义符\r或者\n,否则在使用readLine方法读取数据时,会一直阻塞从而读取失败。如下:
writer.write("第一次来到广州\n");

运行结果

首先运行服务端,再接着运行客户端
服务端Server:
这里写图片描述
客户端Client
这里写图片描述

Socket Android使用

既然scoket能够实现两个程序间的信息传输,很明显在Android下是一种IPC方式。
下面以一个简单跨进程聊天程序为例,功能点能够自动回复。

实现流程

  1. 创建一个远程Service服务,在其建立TCP服务(服务端
  2. 在界面上(Activity、Fragment等)连接TCP服务(客户端
  3. 在Mainfest中声明网络权限及注册Service

具体实现

服务端:

public class TCPServerService extends IntentService {

    private static final String[] defaultMessages = {
            "你好啊,嘻嘻",
            "看了你相片,你好帅哦,很喜欢你这样的",
            "我是江西人,你呢?",
            "你在哪里工作?"};

    private int index = 0;

    private boolean isServiceDestroy = false;
     //需注意,必须传入参数
    public TCPServerService() {
        super("TCP");
    }

    @Override
    public void onCreate() {
        super.onCreate();

    }

    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        try {
            //1、监听本地端口号
            ServerSocket serverSocket = new ServerSocket(8954);
            Socket socket = serverSocket.accept();


            //2、获取输入流,接受用户发来的消息(Activity)
            InputStream inputStream = socket.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

            //3、获取输出流,向客户端(Activity)回复消息
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));

            //4、通过循环不断读取客户端发来的消息 ,并发送
            while (!isServiceDestroy) {
                String readLine = reader.readLine();

                if (!TextUtils.isEmpty(readLine)) {
                    String sendMag = index < defaultMessages.length ? defaultMessages[index] : "已离线";
                    SystemClock.sleep(500); //延迟发送
                    writer.println(sendMag+"\r"); // `\r或\n`必须要有,否则会影响客户端接受消息
                    writer.flush();  //刷新流
                    index++;
                }
            }


            //关闭流
            inputStream.close();
            reader.close();
            outputStream.close();
            writer.close();
            socket.close();
            //需关闭,否则再次连接时,会报端口号已被使用
            serverSocket.close();

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

    @Override
    public void onDestroy() {
        super.onDestroy();
        isServiceDestroy = true;
        Log.d(TAG, "onDestroy: ");
    }

    private static final String TAG = "TCPServerService";
}

客户端:

public class SocketActivity extends AppCompatActivity {

    private TextView mTvChatContent;
    private EditText mEtSendContent;
    private Intent mIntent;

    private static final int CONNECT_SERVER_SUCCESS = 0; //与服务端连接成功
    private static final int MESSAGE_RECEIVE_SUCCESS = 1; //接受到服务端的消息
    private static final int MESSAGE_SEND_SUCCESS=2; //消息发送
    @SuppressLint("all")
    private Handler mHandler = new Handler(new Handler.Callback() {

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case CONNECT_SERVER_SUCCESS:
                    //与服务端连接成功
                    mTvChatContent.setText("与聊天室连接成功\n");
                    break;
                case MESSAGE_RECEIVE_SUCCESS:
                    String msgContent = mTvChatContent.getText().toString();
                    mTvChatContent.setText(msgContent+msg.obj.toString()+"\n");
                    break;
                case MESSAGE_SEND_SUCCESS:
                    mEtSendContent.setText("");
                    mTvChatContent.setText(mTvChatContent.getText().toString()+msg.obj.toString()+"\n");
                    break;
            }
            return false;
        }
    });
    private PrintWriter mPrintWriter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_socket);
        mTvChatContent = findViewById(R.id.tv_chat_content);
        mEtSendContent = findViewById(R.id.et_send_content);

        //启动服务
        mIntent = new Intent(this, TCPServerService.class);
        startService(mIntent);

        new Thread(new Runnable() {
            @Override
            public void run() {
                //连接服务端,实现通信交互
                //IO操作必须放在子线程执行
                connectTCPServer();
            }
        }).start();

    }

    private  Socket mSocket=null;
    private void connectTCPServer() {
        //通过循环来判断Socket是否有被创建,若没有则会每隔1s尝试创建,目的是保证客户端与服务端能够连接
        while (mSocket == null) {
            try {
                //创建Socket对象,指定IP地址和端口号
                mSocket = new Socket("localhost", 8954);
                mPrintWriter = new PrintWriter(new OutputStreamWriter(mSocket.getOutputStream()),true);
                if (mSocket.isConnected()) //判断是否连接成功
                    mHandler.sendEmptyMessage(CONNECT_SERVER_SUCCESS);
            } catch (IOException e) {
                e.printStackTrace();
                //设计休眠机制,每次重试的间隔时间为1s
                SystemClock.sleep(1000);
            }

        }

        //通过循环来,不断的接受服务端发来的消息
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
            while (!SocketActivity.this.isFinishing()){ //当Activity销毁后将不接受
                String msg = reader.readLine();
                if (!TextUtils.isEmpty(msg)){
                    //发消息通知更新UI
                    mHandler.obtainMessage(MESSAGE_RECEIVE_SUCCESS,msg).sendToTarget();
                }

            }
            //关闭流
            mPrintWriter.close();
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }



    }


    @SuppressLint("SetTextI18n")
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.but_send:
                //必须开启子线程,不能在UI线程操作网络
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        String msg = mEtSendContent.getText().toString();
                        if (mPrintWriter!=null && !TextUtils.isEmpty(msg)){
                            mPrintWriter.println(msg+"\n");
                            //此处可以不用刷新流的方法,因为在创建mPrintWriter对象时,在其构造方法中设置了自动刷新缓存
//                            mPrintWriter.flush(); 
                            //通知更新UI
                            mHandler.obtainMessage(MESSAGE_SEND_SUCCESS,msg).sendToTarget();
                        }
                    }
                }).start();


                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //关闭输入流和连接
        if (mSocket!=null){
            try {
                mSocket.shutdownInput();
                mSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //停止后台服务
        stopService(mIntent);
    }

    private static final String TAG = "TCPServerService";
}

Mainfest:

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

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

运行结果:
这里写图片描述

注意事项:
1、在使用IntentService时,必须在重写其构造方法并指定线程名,否则报错has no zero argument constructor如下:

  public TCPServerService( ) {
        super("TCP");
    }

2、连接服务端和发送消息必须放在子线程中执行,否则会报NetworkOnMainThreadException
3、在writer.println()writer.write()方法中传入的字符串必须要有\r\n转义符,同时需刷新流flush(),否则会影响消息及时性
4、服务端在停止之前必须关闭ServerSocket,调用close()即可,否则再次连接时,会报端口号已被使用错误,java.net.BindException: bind failed: EADDRINUSE (Address already in use)
5、客户端Socket与服务端ServerSocket的端口号port必须一致
6、在Mainfest中必须声明网络权限,否则连接失败,提示没有权限socket failed: EACCES (Permission denied)

以上几点是在开发中容易入坑的地方。

参考

猜你喜欢

转载自blog.csdn.net/hzw2017/article/details/81210979