Android中基于ServerSocket的实际使用与简单介绍(内附一个PC端群控多台手机的消息发送和接收Demo)

一、要想将ServerSocket整明白首先至少先要知道或是了解几点基础内容部分(大神或是有一定能力的小神跳过):

1.ServerSocket是需要在服务端定义书写的而在客户端不需要ServerSocket,客户端只需要创建socket就可以了。

2.socket需要在子线程中处理,所以必须开辟子线程,而老版本的socket不需要在子线程中书写,所以网上的一些比较老版本的帖子Demo什么的没有创建子线程,这点需要注意。

3.在Android Studio中创建Java代码,控制台会出现中文乱码的情况(与UTF-8转码无关),我在另一个博客文章中介绍了解决方法(转载的文章),出现乱码的同学可以去找一下。

4.数据的发送与接收都需要通过socket来读取或发送输入输出流。

5.子线程不能改变主线程的UI,所以需要使用Handler,因为比较简单所以以上这些就不过多的讲解了。


二、为了更好的说明和理解Socket通信,我将查到的一些可能常出现的异常和使用方法等进行了总结:

异常类型
在了解Socket的内容之前,先要了解一下涉及到的一些异常类型。以下四种类型都是继承于IOException,所以很多之后直接弹出IOException即可。
UnkownHostException:      主机名字或IP错误
ConnectException:        服务器拒绝连接、服务器没有启动、(超出队列数,拒绝连接)
SocketTimeoutException:      连接超时
BindException:          Socket对象无法与制定的本地IP地址或端口绑定


构造函数
Socket(InetAddress address, int port)throws UnknownHostException, IOException
Socket(InetAddress address, int port, InetAddress localAddress, int localPort)throws IOException
Socket(String host, int port)throws UnknownHostException, IOException
Socket(String host, int port, InetAddress localAddress, int localPort)throws IOException
 

除去第一种不带参数的之外,其它构造函数会尝试建立与服务器的连接。如果失败会抛出IOException错误。如果成功,则返回Socket对象。
InetAddress是一个用于记录主机的类,其静态getHostByName(String msg)可以返回一个实例,其静态方法getLocalHost()也可以获得当前主机的IP地址,并返回一个实例。Socket(String host, int port, InetAddress localAddress, int localPort)构造函数的参数分别为目标IP、目标端口、绑定本地IP、绑定本地端口。
 

Socket方法
getInetAddress();       远程服务端的IP地址
getPort();          远程服务端的端口
getLocalAddress()       本地客户端的IP地址
getLocalPort()        本地客户端的端口
getInputStream();       获得输入流
getOutStream();        获得输出流
值得注意的是,在这些方法里面,最重要的就是getInputStream()和getOutputStream()了。
 
Socket状态
isClosed();             //连接是否已关闭,若关闭,返回true;否则返回false
isConnect();            //如果曾经连接过,返回true;否则返回false
isBound();             //如果Socket已经与本地一个端口绑定,返回true;否则返回false
如果要确认Socket的状态是否处于连接中,下面语句是很好的判断方式。
boolean isConnection=socket.isConnected() && !socket.isClosed();   //判断当前是否处于连接


/**
 * 1.获得远程服务器的IP 地址.
 * InetAddress inetAddress = socket.getInetAddress();
 * 2.获得远程服务器的端口.
 * int port = socket.getPort();
 * 3. 获得客户本地的IP 地址.
 * InetAddress localAddress = socket.getLocalAddress();
 * 4.获得客户本地的端口.
 * int localPort = socket.getLocalPort();
 * 5.获取本地的地址和端口号
 * SocketAddress localSocketAddress = socket.getLocalSocketAddress();
 * 6.获得远程的地址和端口号
 * SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress();
 */

以上的代码是自己找到或是自己整理的关于能更好的加深对socket了解的一些知识点段落,在开始看下面代码之前最好先了解一下。


三、为了更好的进行了解和总结,并且尽量避免和网上的一些Demo博客冲突,所以自己写了两个Demo(一个在PC端中是Java项目暂且叫D1,另一个在手机端是Android项目暂且叫D2)大致功能为:1)一台PC能够连接多台手机进行数据的发送和接收;2)PC端可以显示已连接手机的数量以及ID端口号;3)手机端和PC端的数据发送会得到返回值来表示是否发送接收成功;4)实现群发和单独发送;都是一些比较小的功能,但却是一个大项目的雏形,所以我进行了一个简单的封装来尽量节省代码量和方便后期的修改。话不多说首先介绍D1也就是PC端的Java代码如下所示:

1.创建Java工程(还是尽量讲的详细些吧~):


 

2.创建一个ClientManager的客户端管理类,实现如下代码:

// 管理连接到服务器中的手机类
public class ClientManager {

    private static ServerThread serverThread = null;
    private static int sum = 0;
    private static Map, Socket> clientMap = new HashMap<>();
    private static List clientList = new ArrayList<>();

    private static class ServerThread implements Runnable {

        private ServerSocket server;
        private int port = 10086;
        private boolean isExit = false;// 一个boolean类型的判断 默认是退出状态false

        // 构造方法初始化
        public ServerThread() {
            try {
                server = new ServerSocket(port);
                System.out.println("启动server,端口号:" + port);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        /**
         * 1.获得远程服务器的IP 地址.
         * InetAddress inetAddress = socket.getInetAddress();
         * 2.获得远程服务器的端口.
         * int port = socket.getPort();
         * 3. 获得客户本地的IP 地址.
         * InetAddress localAddress = socket.getLocalAddress();
         * 4.获得客户本地的端口.
         * int localPort = socket.getLocalPort();
         * 5.获取本地的地址和端口号
         * SocketAddress localSocketAddress = socket.getLocalSocketAddress();
         * 6.获得远程的地址和端口号
         * SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress();
         */
        @Override
        public void run() {
            try {
                while (!isExit) {
                    // 等待连接
                    System.out.println("等待手机的连接中... ...");
                    final Socket socket = server.accept();
                    System.out.println("获取的手机IP地址及端口号:" + socket.getRemoteSocketAddress().toString());
                    /**
                     * 因为考虑到多手机连接的情况 所以加入线程锁 只允许单线程工作
                     */
                    new Thread(new Runnable() {

                        private String text;

                        @Override
                        public void run() {
                            try {
                                synchronized (this) {
                                    // 在这里考虑到线程总数的计算 也代表着连接手机的数量
                                    ++sum;
                                    // 存入到集合和Map中为群发和单独发送做准备
                                    String string = socket.getRemoteSocketAddress().toString();
                                    clientList.add(string);
                                    clientMap.put(string, socket);
                                }

                                // 定义输入输出流
                                InputStream is = socket.getInputStream();
                                OutputStream os = socket.getOutputStream();

                                // 接下来考虑输入流的读取显示到PC端和返回是否收到
                                byte[] buffer = new byte[1024];
                                int len;
                                while ((len = is.read(buffer)) != -1) {
                                    text = new String(buffer, 0, len);

                                    System.out.println("收到的数据为:" + text);
                                    os.write("已收到消息".getBytes("utf-8"));

                                }

                            } catch (IOException e) {
                                e.printStackTrace();
                            } finally {
                                System.out.println("关闭连接:" + socket.getRemoteSocketAddress().toString());
                                synchronized (this) {
                                    --sum;
                                    String string = socket.getRemoteSocketAddress().toString();
                                    clientMap.remove(string);
                                    clientList.remove(string);
                                }
                            }
                        }
                    }).start();

                }

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

        // 关闭server
        public void stop() {
            isExit = true;
            if (server != null) {
                try {
                    server.close();
                    System.out.println("已关闭server");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    // 启动server
    public static ServerThread startServer() {
        System.out.println("开启server");
        if (serverThread != null) {
            System.out.println("server不为null正在重启server");
            // 以下为关闭server和socket
            shutDown();
        }
        // 初始化
        serverThread = new ServerThread();
        new Thread(serverThread).start();
        System.out.println("开启server成功");
        return serverThread;
    }


    // 发送消息的方法
    public static boolean sendMessage(String name, String mag) {
        try {
            Socket socket = clientMap.get(name);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(mag.getBytes("utf-8"));
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    // 群发的方法
    public static boolean sendMsgAll(String msg){
        try {
            for (Socket socket : clientMap.values()) {
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write(msg.getBytes("utf-8"));
            }
                return true;
        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }

    // 获取线程总数的方法,也等同于<获取已连接了多少台手机>的方法+
    public static int sumTotal() {
        return sum;
    }

    // 一个获取list集合的方法,取到所有连接server的手机的ip和端口号的集合
    public static List getTotalClients() {
        return clientList;
    }

    public static void shutDown() {
        for (Socket socket : clientMap.values()) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        serverThread.stop();
        clientMap.clear();
        clientList.clear();
    }


}
因为这个博客的代码编辑不是特别的会用,所以直接从我的TestDemo中直接复制了~但是我已经尽可能多的每段都敲好了注释,所以还有什么不懂的地方可以google一下~或者是运行一下代码用debug模式看下代码的执行流程~到这里为止我觉的只需要注意如下几个方面:

(1)socket = serverSocket.accept();像牛郎织女一样~一直在苦苦等待自己要等的人出现...如果不出现就一直等下去,直到等到符合条件的请求出现为止,才会分配出一个socket来让程序向下执行,不然一直持续在阻塞等待状态下(不信你打log试试看)

(2)使用完之后一定记得要关闭server和socket (比关流要重要的多)在代码中会有大量的判断要考虑,我写的还不是很成熟很多地方还没有考虑完美,但是能实现想要的结果。

(3)synchronized的使用在于优化对线程的控制,因为是单排车道通过,所以可以从中获取通过了多少个子线程(也就意味着获取了连接手机的数量),因为每台手机的IP及端口号都不一致,所以通过这个来存储到Map中来实现单独指定手机发送数据的功能,存到Map中的key为IP端口号、value为分配的socket,从而可以通过遍历Map来实现群发的功能,所以synchronized是不可或缺的。


3.到MyClass中,这个名字是创建工程的时候默认哦~所以不要说我命名不规范~

public class MyClass {

    public static void main(String[]args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line;
        boolean isExit = false;

        ClientManager.startServer();
        while (!isExit){
            line = br.readLine();
            if (line.startsWith("exit")){
                System.out.println("退出命令");
                break;
            }

            if (line.startsWith("send")){
                sendMessage(line);
            }else if (line.startsWith("list")){
                printTotal();
            }else if (line.startsWith("all")){
                allSendMsg(line);
            }else {
                System.out.println("输入错误 请重新输入");
            }

        }
        // 关闭 清空
        ClientManager.shutDown();

    }

    private static void allSendMsg(String line) {
        String[] field = line.split("//");
        if (field.length == 2){
            ClientManager.sendMsgAll(field[1]);
            System.out.println("发送结果为:" + ClientManager.sendMsgAll(field[1]) );
        }else {
            System.out.println("格式不正确 例:all//message");
        }
    }

    private static void printTotal() {
        List totalClients = ClientManager.getTotalClients();
        System.out.println("连接数量为:" + totalClients.size());
        for (String totalClient : totalClients) {
            System.out.println(totalClient);
        }
    }

    private static void sendMessage(String line) {
        String[] field = line.split("//");
        if (field.length == 3){
            // 格式正确
            ClientManager.sendMessage(field[1],field[2]);
            System.out.println("send结果为:" + ClientManager.sendMessage(field[1],field[2]));
        }else {
            System.out.println("命令不正确。例子:send//name//msg");
        }
    }

}
已上的这些代码我觉的应该没啥大问题,因为自己写的东西实在是有点浅薄... 敲代码其实有时就像是在写书法一样,有的人代码打开会让你看的陶醉和钦佩,而有的人的代码就让人感觉不舒服跟看小学生的字帖儿一样..我可能就是后者吧 = =' 有不懂的地方可以自己google一下~我就不多说明了 。


4.接下来是创建D2,也就是Android项目,首先我建议先把权限加上不要忘了:

android:name="android.permission.INTERNET"/>
android:name="android.permission.ACCESS_WIFI_STATE" />
android:name="android.permission.ACCESS_NETWORK_STATE"/>

5.Android项目我就没有封装,直接想写啥就写啥了,但是代码不多很方便理解,如下所示:

public class MainActivity extends AppCompatActivity {

    private Button btn;
    private EditText et;
    private TextView tv;
//    private WifiManager w = null;
    private Socket socket;

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

        btn = (Button) findViewById(R.id.btn);
        et = (EditText) findViewById(R.id.et_send);
        tv = (TextView) findViewById(R.id.tv_js);

        final Handler handler = new MyHandler();


        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    socket = new Socket("192.168.1.111", 10086);
                    // 接收
                    InputStream inputStream = socket.getInputStream();
                    byte[] buffer = new byte[102400];
                    int len;
                    while ((len = inputStream.read(buffer)) != -1) {
                        String s = new String(buffer, 0, len);
                        //
                        Message message = Message.obtain();
                        message.what = 0;
                        message.obj = s;
                        handler.sendMessage(message);
                    }


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

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String string = et.getText().toString();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            // 发送
                            OutputStream outputStream = null;
                            outputStream = socket.getOutputStream();
                            outputStream.write(("IP:" + getHostIp() + " " + string).getBytes("utf-8"));
                            outputStream.flush();// 清空缓存

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

                    }
                }).start();
            }
        });

    }

// 获取IP并转换格式
    private String getHostIp() {

        WifiManager mg = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        if (mg == null){
            return "";
        }

        WifiInfo wifiInfo = mg.getConnectionInfo();
        int ip = wifiInfo.getIpAddress();
        return ((ip & 0xff) + "." + (ip >> 8 & 0xff) + "."
                + (ip >> 16 & 0xff) + "." + (ip >> 24 & 0xff));
    }

    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 0) {
                String s = (String) msg.obj;
                tv.setText(s);
            }
        }

    }
}


以上代码值得注意的是如下几点:

1)不能再子线程中刷新UI,通过Handler来发送msg;

2)通过WifiManager来获取IP结果需要转换格式,就好像系统的日期是一串数字要通过转换格式变成我们能看懂的样式一样(那串转换格式的代码我也是从网上Ctrl + C、Ctrl + V来的)

3)socket = new Socket("192.168.1.111", 10086);中第一个参数填的是PC端的IP地址,第二个参数要与PC中Server设置的端口号一致;

4)如果wifi环境不好,可以像下图代码一样设置超时时间来进行其它操作:

socket = new Socket();
SocketAddress socAddress = new InetSocketAddress(dsName, dstPort);
// 设置超时时间
socket.connect(socAddress, 5000);
Log.i(TAG, "Connect OK!");


6.Android端的XML文件代码如下图所示:

xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="sq.can_26_socket.MainActivity">

            android:id="@+id/tv_js"
        android:layout_margin="10dp"
        android:textSize="20sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

            android:id="@+id/et_send"
        android:hint="@string/xx"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    


上方的TextView用来显示从PC端接收到的数据,EditText用来获取发送到PC端的消息。综上所有就可以运行你的代码啦~先运行PC端的Java代码,在运行Android项目到手机中,可以把APK打包到多台手机进行测试。最后测试的效果如下图所示:

1)首先运行PC端操作台会有如下的显示结果:



2)打开手机中的项目后,操作台会显示(连接数量的上线我不知道是多少):



3)在操作台上输入如下绿色的指令后进行群发操作,向两台手机发送“这是群发的消息”,显示如下:



4)手机接收到的消息后,显示的效果如下图所示:

Android中基于ServerSocket的实际使用与简单介绍(内附一个PC端群控多台手机的消息发送和接收Demo)_第1张图片


5)手机向电脑发送消息后,操作台显示的效果如下图所示(显示发送消息的手机的IP地址和Message内容):


6)向指定手机发送消息,操作台上编辑如下图绿色部分的内容,执行发送命令:



7)手机上接收后显示效果如下所示:

Android中基于ServerSocket的实际使用与简单介绍(内附一个PC端群控多台手机的消息发送和接收Demo)_第2张图片

总结:综上所述就是Server Socket的简单介绍和部分应用场景的Demo,我将这两个Demo上传到Guthub上,有需要的同学可以点击<这里这里这里>进行下载~ 如果哪里有错误欢迎批评指正~如果对我有什么建议可以留言交流~我会一一回复的~下篇博客要写啥腻... ... 有点不知所以然啊,看着那些大神大牛的博客,感觉都是好高深呀~好像不太适合接触Android不久的我和一些新手朋友们,所以就像我的签名一样,希望通过一篇篇的博客来逐渐的走向大牛或是架构师的人生巅峰之中~~



你可能感兴趣的:(安卓基础)