android进阶3step2:Android App通信——Socket通信

改:https://www.jianshu.com/p/089fb79e308b

掌握

Android中Socket编程,包括TCPUDP通信协议,以及加密传输、身份认证的网络协议Https的相关知识。

先扫一下盲:
什么是观察者模式 

端口号IP等网络基础知识扫盲

网络基础知识Http、Https

前言

  • Socket的使用在 Android网络编程中非常重要
  • 今天我将带大家全面了解 Socket 及 其使用方法 

目录

1.网络基础


1.1 计算机网络分层

计算机网络分为五层:物理层、数据链路层、网络层、运输层、应用层

其中:

  • 网络层:负责根据IP找到目的地址的主机
  • 运输层:通过端口把数据传到目的主机的目的进程,来实现进程与进程之间的通信

1.2 端口号(PORT)

端口号规定为16位,即允许一个IP主机有2的16次方65535个不同的端口。其中:

  • 0~1023:分配给系统的端口号 

我们不可以乱用

 

  • 1024~49151:登记端口号,主要是让第三方应用使用

但是必须在IANA(互联网数字分配机构)按照规定手续登记,

  • 49152~65535:短暂端口号,是留给客户进程选择暂时使用,一个进程使用完就可以供其他进程使用。

Socket使用时,可以用1024~65535的端口号



1.3 C/S结构

  • 定义:即客户端/服务器结构,是软件系统体系结构
  • 作用:充分利用两端硬件环境的优势,将任务合理分配到Client端和Server端来实现,降低了系统的通讯开销。 

Socket正是使用这种结构建立连接的,一个套接字接客户端,一个套接字接服务器。

如图:


可以看出,Socket的使用可以基于TCP或者UDP协议。



1.4 TCP协议

  • 定义:Transmission Control Protocol,即传输控制协议,是一种传输层通信协议 

基于TCP的应用层协议有FTP、Telnet、SMTP、HTTP、POP3与DNS。

  • 特点:面向连接、面向字节流、全双工通信、可靠
  1. 面向连接:指的是要使用TCP传输数据,必须先建立TCP连接,传输完成后释放连接,就像打电话一样必须先拨号建立一条连接,打完后挂机释放连接。
  2. 全双工通信:即一旦建立了TCP连接,通信双方可以在任何时候都能发送数据。
  3. 可靠的:指的是通过TCP连接传送的数据,无差错,不丢失,不重复,并且按序到达。
  4. 面向字节流:流,指的是流入到进程或从进程流出的字符序列。简单来说,虽然有时候要传输的数据流太大,TCP报文长度有限制,不能一次传输完,要把它分为好几个数据块,但是由于可靠性保证,接收方可以按顺序接收数据块然后重新组成分块之前的数据流,所以TCP看起来就像直接互相传输字节流一样,面向字节流。
  • TCP建立连接 

必须进行三次握手:若A要与B进行连接,则必须 

  1. 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认。即A发送信息给B
  2. 第二次握手:服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认。即B收到连接信息后向A返回确认信息
  3. 第三次握手:客户端收到服务器的(SYN+ACK)报文段,并向服务器发送ACK报文段。即A收到确认信息后再次向B返回确认连接信息 

此时,A告诉自己上层连接建立;B收到连接信息后告诉上层连接建立。


这样就完成TCP三次握手 = 一条TCP连接建立完成 = 可以开始发送数据

三次握手期间任何一次未收到对面回复都要重发。
最后一个确认报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态。


为什么TCP建立连接需要三次握手?

答:

防止服务器端因为接收了早已失效的连接请求报文从而一直等待客户端请求,从而浪费资源

  • “已失效的连接请求报文段”的产生在这样一种情况下:Client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。
  • 这是一个早已失效的报文段。但Server收到此失效的连接请求报文段后,就误认为是Client再次发出的一个新的连接请求。
  • 于是就向Client发出确认报文段,同意建立连接。
  • 假设不采用“三次握手”:只要Server发出确认,新的连接就建立了。
  • 由于现在Client并没有发出建立连接的请求,因此不会向Server发送数据。
  • 但Server却以为新的运输连接已经建立,并一直等待Client发来数据。>- 这样,Server的资源就白白浪费掉了。

采用“三次握手”的办法可以防止上述现象发生:

  • Client不会向Server的确认发出确认
  • Server由于收不到确认,就知道Client并没有要求建立连接
  • 所以Server不会等待Client发送数据,资源就没有被浪费

 

TCP释放连接 

  • TCP释放连接需要四次挥手过程,现在假设A主动释放连接:(数据传输结束后,通信的双方都可释放连接)
  1. 第一次挥手:A发送释放信息到B;(发出去之后,A->B发送数据这条路径就断了)
  2. 第二次挥手:B收到A的释放信息之后,回复确认释放的信息:我同意你的释放连接请求
  3. 第三次挥手:B发送“请求释放连接“信息给A
  4. 第四次挥手:A收到B发送的信息后向B发送确认释放信息:我同意你的释放连接请求

B收到确认信息后就会正式关闭连接; 
A等待2MSL后依然没有收到回复,则证明B端已正常关闭,于是A关闭连接


为什么TCP释放连接需要四次挥手?

为了保证双方都能通知对方“需要释放连接”,即在释放连接后都无法接收或发送消息给对方

  • 需要明确的是:TCP是全双工模式,这意味着是双向都可以发送、接收的
  • 释放连接的定义是:双方都无法接收或发送消息给对方,是双向的
  • 当主机1发出“释放连接请求”(FIN报文段)时,只是表示主机1已经没有数据要发送 / 数据已经全部发送完毕; 

 但是,这个时候主机1还是可以接受来自主机2的数据。

  • 当主机2返回“确认释放连接”信息(ACK报文段)时,表示它已经知道主机1没有数据发送了 
  • 但此时主机2还是可以发送数据给主机1
  • 当主机2也发送了FIN报文段时,即告诉主机1我也没有数据要发送了 
  • 此时,主机1和2已经无法进行通信:主机1无法发送数据给主机2,主机2也无法发送数据给主机1,此时,TCP的连接才算释放

1.5 UDP协议

  • 定义:User Datagram Protocol,即用户数据报协议,是一种传输层通信协议。

基于UDP的应用层协议有TFTP、SNMP与DNS。

  • 特点:无连接的、不可靠的、面向报文、没有拥塞控制
  1. 无连接的:和TCP要建立连接不同,UDP传输数据不需要建立连接,就像写信,在信封写上收信人名称、地址就可以交给邮局发送了,至于能不能送到,就要看邮局的送信能力和送信过程的困难程度了。
  2. 不可靠的:因为UDP发出去的数据包发出去就不管了,不管它会不会到达,所以很可能会出现丢包现象,使传输的数据出错。
  3. 面向报文:数据报文,就相当于一个数据包,应用层交给UDP多大的数据包,UDP就照样发送,不会像TCP那样拆分。
  4. 没有拥塞控制:拥塞,是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象,就像交通堵塞一样。TCP建立连接后如果发送的数据因为信道质量的原因不能到达目的地,它会不断重发,有可能导致越来越塞,所以需要一个复杂的原理来控制拥塞。而UDP就没有这个烦恼,发出去就不管了。

应用场景

 很多的实时应用(如IP电话、实时视频会议、某些多人同时在线游戏等)要求源主机以很定的速率发送数据,并且允许在网络发生拥塞时候丢失一些数据,但是要求不能有太大的延时,UDP就刚好适合这种要求。所以说,只有不适合的技术,没有真正没用的技术。


 1.6 HTTP协议

详情请看我写的另外一篇文章你需要了解的HTTP知识都在这里了!


2. Socket定义

  • 即套接字,是一个对 TCP / IP协议进行封装 的编程调用接口(API)
  1. 即通过Socket,我们才能在Andorid平台上通过 TCP/IP协议进行开发
  2. Socket不是一种协议,而是一个编程调用接口(API),属于传输层(主要解决数据如何在网络中传输)
  • 成对出现,一对套接字:
  1. Socket ={(IP地址1:PORT端口号),(IP地址2:PORT端口号)}

 3. 原理

Socket的使用类型主要有两种:

  • 流套接字(streamsocket) :基于 TCP协议,采用 流的方式 提供可靠的字节流服务
  • 数据报套接字(datagramsocket):基于 UDP协议,采用 数据报文 提供数据打包发送的服务

具体原理图如下:

 

4. Socket 与 Http 对比

  • Socket属于传输层,因为 TCP / IP协议属于传输层,解决的是数据如何在网络中传输的问题
  • HTTP协议 属于 应用层,解决的是如何包装数据

由于二者不属于同一层面,所以本来是没有可比性的。但随着发展,默认的Http里封装了下面几层的使用,所以才会出现Socket & HTTP协议的对比:(主要是工作方式的不同):

  • Http:采用 请求—响应 方式。

即建立网络连接后,当 客户端 向 服务器 发送请求后,服务器端才能向客户端返回数据。
可理解为:是客户端有需要才进行通信

  • Socket:采用 服务器主动发送数据 的方式

即建立网络连接后,服务器可主动发送消息给客户端,而不需要由客户端向服务器发送请求
可理解为:是服务器端有需要才进行通信


5. 使用步骤

  • Socket可基于TCP或者UDP协议,但TCP更加常用
  • 所以下面的使用步骤 & 实例的Socket将基于UDP协议

UDP 使用API介绍

  • – InetAddress(确定是哪个地址)
  • – DatagramSocket(receive,send) (数据的发送和接受)
  • – DatagramPacket(数据打包)

• 实战

  • – Client与Server通信
  • 发送流程固定:必须先Client发送一条数据给Server,Server再发送消息给Client

1-使用UDP(实现客户端和服务端的通信)

服务端UdpServer.java的编写 

主要是接受client发来的消息和发送给client的消息

涉及DatagramSocket、DatagramPacket、InetAddress三个API



import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 10:32<p>
 * <p>更改时间:2018/11/26 10:32<p>
 * <p>版本号:1<p>
 */

/***
 * 服务端的代码
 */
public class UdpServer {

    private InetAddress mInetAddress;//确定是哪个地址
    private int mPort = 8888;//端口号:确定是哪个应用
    private Scanner mScanner;

    private DatagramSocket mSocket;

    public UdpServer() {
        try {
            mInetAddress = InetAddress.getLocalHost();//基于本机的地址
            System.out.println(mInetAddress);
            mSocket = new DatagramSocket(mPort, mInetAddress);
            mScanner = new Scanner(System.in);//结束输入数据
            mScanner.useDelimiter("\n");//换行结束

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    public void start() {

        while (true) {//不要server退出
            try {
                /**
                 * Server接受client发来的的数据
                 */
                byte[] buf = new byte[1024];
                //1.准备装数据的包
                //数组,数组长度
                DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);
                //2.接受数据包(如果没有数据会一直处于阻塞状态)
                mSocket.receive(receivedPacket);
                //3.解析数据包
                //拿地址/端口号
                InetAddress address = receivedPacket.getAddress();
                int port = receivedPacket.getPort();
                //byte[]数组解析成String
                String clientMsg = new String(
                        receivedPacket.getData(),
                        0,
                        receivedPacket.getLength());
                System.out.println("收到Client发来的数据:address=" + address + ",port=" + port + ",msg=" + clientMsg);

                /**
                 * Server发送数据到client
                 */
                String returnMsg = mScanner.next();
                System.out.println("Server发送数据给Client:"+returnMsg);
                byte[] returnMsgBytes = returnMsg.getBytes();
                //数组。数组长度,携带目标地址
                DatagramPacket sendPacket = new DatagramPacket(
                        returnMsgBytes,
                        returnMsgBytes.length,
                        receivedPacket.getSocketAddress());
                mSocket.send(sendPacket);

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

    /**
     * 启动服务端
     *
     * @param args
     */
    public static void main(String[] args) {
        new UdpServer().start();
    }
}

客户端UdpClient.java的编写 


/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 10:51<p>
 * <p>更改时间:2018/11/26 10:51<p>
 * <p>版本号:1<p>
 */

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/***
 * 客户端的编写
 */
public class UdpClient {
    //Serverip地址
    private String mServerIp = "192.168.31.180";
    private InetAddress mServerAddress;
    //Server的端口号
    private int mServerPort = 8888;
    private DatagramSocket mSocket;
    private Scanner mScanner;

    public UdpClient() {
        try {
            mServerAddress = InetAddress.getByName(mServerIp);
            mSocket = new DatagramSocket();
            mScanner = new Scanner(System.in);
            mScanner.useDelimiter("\n");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void start() {
        while (true) {
            try {
                /**
                 * Client发送数据给Server
                 */
                String ClientMsg = mScanner.next();
                System.out.println("Client发送数据给Server" + ClientMsg);
                byte[] clientMsgBytes = ClientMsg.getBytes();
                //byte数组,长度,server端的地址,server端的端口号
                DatagramPacket ClientPacket = new DatagramPacket(
                        clientMsgBytes
                        , clientMsgBytes.length, mServerAddress, mServerPort);
                mSocket.send(ClientPacket);
                /**
                 * 接收Server的数据
                 */
                byte[] buf = new byte[1024];
                DatagramPacket ServerMsgPacket = new DatagramPacket(buf, buf.length);
                mSocket.receive(ServerMsgPacket);
                //解析数据包
                //byte[]数组解析成String
                String ServerMsg = new String(
                        ServerMsgPacket.getData(),
                        0,
                        ServerMsgPacket.getLength());
                System.out.println("收到Server发来的 msg= " + ServerMsg);


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

        }
    }

    /**
     * 程序的入口
     *
     * @param args
     */
    public static void main(String[] args) {
        new UdpClient().start();
    }
}

演示:如果Server端一直报空指针,说明端口号被占用,换一个端口号即可(注意client和server要写同一个端口号)

run main函数后 Client端:


 

Server端: 

移植客户端到android中

服务端不变:

第一步:androidmanifest.xml中添加网络权限

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

第二步:建biz业务类 实现客户端的数据发送和接收

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 10:51<p>
 * <p>更改时间:2018/11/26 10:51<p>
 * <p>版本号:1<p>
 */

import android.text.TextUtils;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/***
 * 客户端的编写
 */
public class UdpClientBiz {
    //Serverip地址
    private String mServerIp = "192.168.31.180";
    private InetAddress mServerAddress;
    //Server的端口号
    private int mServerPort = 9999;
    private DatagramSocket mSocket;

    public UdpClientBiz() {
        try {
            mServerAddress = InetAddress.getByName(mServerIp);
            mSocket = new DatagramSocket();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回调函数
     */
    public interface OnMsgReturnListener {
        void OnMsgReturnListener(String msg);

        void onError(Exception ex);
    }

    /**
     * 提供外接send方法
     *
     * @param msg
     * @param listener
     */
    public void sendMsg(final String msg, final OnMsgReturnListener listener) {
        //校验
        if (!TextUtils.isEmpty(msg) || listener != null) {
            new Thread() {
                @Override
                public void run() {
                    /**
                     * Client发送数据给Server
                     */
                    try {
                        System.out.println("Client发送数据给Server" + msg);
                        byte[] clientMsgBytes = msg.getBytes();
                        //byte数组,长度,server端的地址,server端的端口号
                        DatagramPacket ClientPacket = new DatagramPacket(
                                clientMsgBytes
                                , clientMsgBytes.length, mServerAddress, mServerPort);
                        mSocket.send(ClientPacket);
                        //receiver msg
                        /**
                         * 接收Server的数据
                         */
                        byte[] buf = new byte[1024];
                        DatagramPacket ServerMsgPacket = new DatagramPacket(buf, buf.length);
                        mSocket.receive(ServerMsgPacket);
                        //解析数据包
                        //byte[]数组解析成String
                        String ServerMsg = new String(
                                ServerMsgPacket.getData(),
                                0,
                                ServerMsgPacket.getLength());
                        System.out.println("收到Server发来的 msg= " + ServerMsg);
                       ///通过listener回调出去
                        listener.OnMsgReturnListener(ServerMsg);
                    } catch (IOException e) {
                        //异步回调exception回调出去
                        listener.onError(e);
                    }
                }
            }.start();


        }
    }

    /**
     * 关闭Socket
     */
    public void onDestroy() {
        if (mSocket != null) {
            mSocket.close();
        }
    }
}

 第三步:MainActivity.java


import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.demo.udpdemo.biz.UdpClientBiz;

public class MainActivity extends AppCompatActivity {

    private Button bt_send;
    private EditText et_msg;
    private TextView tv_content;
    private UdpClientBiz mUdpClientBiz = new UdpClientBiz();
    //UIHandler
    private Handler mUiHandler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        initEvent();
    }

    /**
     * 初始化事件
     */
    private void initEvent() {
        bt_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String msg = et_msg.getText().toString();
                if (TextUtils.isEmpty(msg)) {
                    return;
                }
                /**
                 * client发送消息也要append一下
                 */
                appendMsgToContent("client——>server: " + msg);
                /**
                 * 发送消息到server,并且拿server返回的 信息
                 */
                //发送完之后清空一下输入框
                et_msg.setText("");
                mUdpClientBiz.sendMsg(msg, new UdpClientBiz.OnMsgReturnListener() {
                    @Override
                    public void OnMsgReturnListener(final String msg) {
                        //收到server发送的数据
                        //更新UI,让它回到主线程中更新ui
                        mUiHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                appendMsgToContent("server——>client: " + msg);
                            }
                        });

                    }

                    @Override
                    public void onError(Exception ex) {
                        ex.printStackTrace();
                    }
                });
            }
        });
    }

    /**
     * 显示内容的拼接方法
     */

    private void appendMsgToContent(String msg) {
        tv_content.append(msg + "\n");

    }

    /**
     * 初始化视图
     */
    private void initViews() {
        bt_send = findViewById(R.id.id_bt_send);
        et_msg = findViewById(R.id.id_et_msg);
        tv_content = findViewById(R.id.id_tv_content);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mUdpClientBiz.onDestroy();
    }
}

 完成。

2-通过TCP实现网络连接(简易聊天室 多个客户端)

上面的例子使用了UDP进行客户端和服务端通信

下面的是使用TCP来实现

  • 多个客户端可以同时发消息给server服务端
  • 服务端将消息同步显示到每一个客户端

TCP 注意的API介绍

  • – ServerSocket
  • – Socket

• 实战

  • – 简易版本聊天室
  • – 涉及:消息队列,多线程处理、观察者模式等;

服务端的编写:

 大致流程

  • 服务端开启,一直处于阻塞状态等待客户端的到来
  • 一旦有客户端连接到服务端,便会开启一个线程来读写数据,并为客户端注册观察者
  • 如果客户端发送了消息,这条消息会发送到消息队列Queue中,并转发到其他注册的观察者(客户端中)

队列有什么作用?:可以解决并发问题(同一时间多个客户端发消息),队列会只开一个线程进行,消息的处理

假设有多个客户端同时进行发送消息,那么消息会分别放入队列中,FIFO的顺序通过一个线程进行转发消息

  1. 服务端立刻将消息放入队列中(如果没有消息会消息队列处于阻塞状态)
  2. 服务端(被观察者)利用观察者模式通知每一个注册的观察者(客户端要注册观察者) 消息来了
  • 所有的客户端就可以收到新的消息(注册观察者的回调方法返回新的数据),数据写入到客户端显示

TcpServer.java


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 13:46<p>
 * <p>更改时间:2018/11/26 13:46<p>
 * <p>版本号:1<p>
 */
public class TcpServer {

    public void start() {

        ServerSocket serverSocket = null;
        try {
            //端口自定
            serverSocket = new ServerSocket(9090);
            /**
             * 1起一个Queue起一个线程不断的从消息队列中取数据正常情况使处于阻塞状态
             */
            MsgPool.getInstance().start();

            while (true) {
                /**
                 * 2.等待客户端的到来 accept是阻塞的方法, socket代表客户端
                 * 如果没有线程到来accept一直处于阻塞状态
                 */
                Socket socket = serverSocket.accept();
                System.out.println(" 当前客户端的信息:ip:" + socket.getInetAddress().getHostAddress() +
                        " port:" + socket.getPort() + " is online...");
                //针对每一个连接客户端创建一个Thread线程 进行读取数据
                //然后继续回到accept中阻塞,等待下一个客户端的连接
                ClientTask clientTask = new ClientTask(socket);
                //每一个客户端注册一个观察者,监听消息的变化
                MsgPool.getInstance().addMsgComingListener(clientTask);
                //每一个客户端都开启一个线程
                clientTask.start();
            }

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

    public static void main(String[] args) {
        new TcpServer().start();
    }

}

MsgPool.java 消息队列类 实现观察者模式

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 14:02<p>
 * <p>更改时间:2018/11/26 14:02<p>
 * <p>版本号:1<p>
 */

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 这是一个消息队列 来解决并发的问题
 * 并发:就是同一时间有多个客户端进行发送消息(就会有很多个线程开启)
 * 不断的往消息队列进行写数据,只有一个线程从这个队列取数据,和分发数据
 */
public class MsgPool {
    /**
     * 单例
     */
    private static MsgPool sInstance = new MsgPool();
    //阻塞的消息队列  可以封装对象
    private LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();

    public static MsgPool getInstance() {
        return sInstance;
    }

    private MsgPool() {
    }

    /**
     * 问题:所有客户端将消息放到队列中,那么队列取出来的消息如何给所有的客户端呢?
     * 使用观察者模式
     * 1.被观察者: MsgPool
     * 2.观察者: 所有的ClientTask
     * 3.观察者注册后,当MsgPool收到消息notify所有的观察者
     */
    /**
     * 1.观察者
     */
    public interface MsgComingListener {
        void onMsgComing(String msg);
    }

    /**
     * 2.存放所有观察者对象的集合
     */
    private List<MsgComingListener> mListeners = new ArrayList<>();

    /**
     * 3.注册观察者的方法
     */
    public void addMsgComingListener(MsgComingListener listener) {
        mListeners.add(listener);
    }

    /**
     * 4.通知所有观察者 有消息来了
     */
    private void notifyMsgComing(String msg) {
        //遍历所有的观察者,执行onMsgComing的方法
        for (MsgComingListener listener : mListeners) {
            listener.onMsgComing(msg);
        }
    }

    /**
     * 开启线程去队列不断读数据
     */

    public void start() {
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        //没有消息会阻塞
                        String msg = mQueue.take();
                        //拿到数据后通知所有观察者 有消息来了
                        notifyMsgComing(msg);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }


    /**
     * 所有客户端将消息放到队列中
     * 如果有消息来,进行取数据分发数据
     * 没有的话阻塞队列就会阻塞
     * 问题:所有客户端将消息放到队列中,那么队列取出来的消息如何给所有的客户端呢?
     */

    public void sendMsg(String msg) {
        try {
            mQueue.put(msg);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

当有客户端连接服务端时,会为该客户端单独开启一个线程

ClientTask.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 13:51<p>
 * <p>更改时间:2018/11/26 13:51<p>
 * <p>版本号:1<p>
 */

/**
 * 每一个连接的客户端单独开一个线程
 * 1.inputstream 读信息
 * 2.outputstream 写信息
 */
public class ClientTask extends Thread implements MsgPool.MsgComingListener {
    //传入Socket对象,可以进行读写数据
    private Socket mSocket;
    private InputStream mIs;
    private OutputStream mOs;

    public ClientTask(Socket socket) {
        try {
            mSocket = socket;
            mIs = socket.getInputStream();
            mOs = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        BufferedReader br = new BufferedReader(new InputStreamReader(mIs));
        String line = null;

        /**读客户端写入的信息
         * 按行读/不断读的消息
         */
        try {
            while ((line = br.readLine()) != null) {
                System.out.println("read= " + line);
                //转发消息到其他客户端socket
                MsgPool.getInstance().sendMsg(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 每一个客户端都是观察者,消息来了 notify所有观察者进行回调
     *
     * @param msg
     */
    @Override
    public void onMsgComing(String msg) {
        //拿到server新的消息,写入数据

        try {
            mOs.write(msg.getBytes());
            mOs.write("\n".getBytes());
            mOs.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端的编写

TcpClient.java 主要是实现

  • 1.写入数据到服务端
  • 2.收到服务端消息队列返回的数据,进行显示
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 14:36<p>
 * <p>更改时间:2018/11/26 14:36<p>
 * <p>版本号:1<p>
 */
public class TcpClient {

    private Scanner mScanner;

    public TcpClient() {
        mScanner = new Scanner(System.in);
        mScanner.useDelimiter("\n");
    }

    public void start() {
        //ip地址/端口号
        try {
            Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(), 9090);
            InputStream is = socket.getInputStream();//输入流读数据
            OutputStream os = socket.getOutputStream();//输出流写数据

            final BufferedReader br = new BufferedReader(new InputStreamReader(is));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
            //开启一个线程读server的数据
            /**
             * 输出服务端发送的数据
             */
            new Thread() {
                @Override
                public void run() {
                    try {
                        String line = null;
                        while ((line = br.readLine()) != null) {
                            System.out.println("收到Server发来的数据" + line);

                        }
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }

                }
            }.start();

            /**
             * 写数据到服务端
             */
            while (true) {
                String msg = mScanner.next();
                bw.write(msg);
                bw.newLine();
                bw.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TcpClient().start();
    }
}

效果:

执行客户端和服务端的Main方法

开启一个服务端,三个客户端

客户端依次输入一个消息 client * send *

看服务端和每个服务端接收到数据:

server端

client1: 

client2:

client3: 

客户端向服务端发送数据的步骤:

  1. 创建客户端与服务端的连接
  2. 从Socket获取输出流的对象
  3. 向输出流对象写入数据
  4. 发送数据到服务器
  5. 断开客户端与服务端的连接

客户端接收服务端数据的步骤:

  1. 创建客户端与服务端的连接
  2. 创建获取输入流对象
  3. 传入输入流
  4. 通过输入流读取器对象接收服务器发送的数据
  5. 断开客户端与服务端的连接

移植客户端(到android上)

注意:添加网络权限

import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;

/**
 * <p>文件描述:<p>
 * <p>作者:Mr-Donkey<p>
 * <p>创建时间:2018/11/26 14:36<p>
 * <p>更改时间:2018/11/26 14:36<p>
 * <p>版本号:1<p>
 */
public class TcpClientBiz {
    private Socket mSocket;
    private InputStream mIs;
    private OutputStream mOs;
    private Handler mUiHandler = new Handler(Looper.getMainLooper());

    /**
     * 消息回调方法
     */
    public interface OnMsgComingListener {
        void onMsgComing(String msg);

        void onError(Exception ex);
    }

    private OnMsgComingListener mListener;

    public void setOnMsgComingListener(OnMsgComingListener listener) {
        mListener = listener;
    }

    public TcpClientBiz() {
        /**
         * 在子线程中进行网络操作
         */
        new Thread() {
            @Override
            public void run() {
                try {
                    //1、创建客户端Socket,指定服务器地址和端口
                    mSocket = new Socket("192.168.31.180", 9090);
                    mIs = mSocket.getInputStream();//输入流读数据
                    mOs = mSocket.getOutputStream();//输出流写数据

                    readServerMsg();//循环接收发送的消息
                } catch (final IOException e) {

                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (mListener != null) {
                                mListener.onError(e);
                                Log.e("TAG-vv",e.getMessage());
                            }
                        }
                    });
                }
            }
        }.start();

    }

    /**
     * 循环接收Server发送的消息
     *
     * @throws IOException
     */
    private void readServerMsg() throws IOException {
        final BufferedReader br = new BufferedReader(new InputStreamReader(mIs));
        String line = null;
        //循环接收server发送来的数据
        while ((line = br.readLine()) != null) {
            //将回调返回到ui线程进行更新ui
            final String finalLine = line;
            mUiHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (mListener != null) {
                        Log.e("TAG-vv", "mUiHandler");
                        mListener.onMsgComing(finalLine);
                    }
                }
            });

        }
    }

    /**
     * 对外提供一个发送数据的方法
     *
     * @param msg
     */
    public void sendMsg(final String msg) {
        new Thread() {
            @Override
            public void run() {
                try {
                    //直接写入server端
                    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(mOs));
                    bw.write(msg);
                    bw.newLine();
                    bw.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }.start();
    }

    public void onDestroy() {
        if (mOs != null) {
            try {
                mOs.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (mIs != null) {
            try {
                mIs.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (mSocket != null) {
            try {
                mSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 开启两个客户端。

服务端

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值