zoukankan      html  css  js  c++  java
  • Android7.1 与 PC端 套接字 socket Tcp/Ip 通信(多线程)

    最近有点闲,打算通过 socket 实现 PC 调用手机摄像头,实现实时视频传输。搞到后来发现搞不了, java 和 c++ 的基本数据类型就有差别,要经过转换才行,自然二者的套接字也是不能直接传数据的。我在手机上运行自己写的客户端,PC 上运行C++开发的服务端,能连接上但是传输不了数据(不能直接传输)。所以不搞了,太麻烦。。。是我太菜哈哈哈哈哈,,,,,

    不过还是把最后结果在这里分享一下,因为这个过程那叫个坎坷哦,网上的确能找到很多这方面的博客什么的,但是没有一个能跑的通的代码,不知道是不是Android版本不同导致的(参考的那些帖也没有给出开发环境),反正我手机上就是运行不了(确切说是能运行,但发不了数据,老是崩溃), 花了  ≈Android小白 的我好几天时间才发现问题所在,没错!是的!!线程的问题!!!

    由于参考了不知道多少个博客和帖子,所以这里不一一列出了,说实话我自己都记不清看(抄)了多少代码了。。。那些代码大部分都是开了一共两个线程,即一个主线程用于UI显示,一个子线程用于建立 socket 连接并传送数据。但我在我手机上运行时总是崩溃,具体表现为:UI能正常显示;能通过socket建立连接;不能发送数据,一点击发送按钮,程序就闪退。开始我以为是我哪里设置的不对,后来搞了好几天,把代码改的面目全非,结构都变了,还是不行,而且不知道是客户端的问题还是服务器的问题。

    后来呢,柳暗花明,机智(愚蠢)的我终于还是弄清了原因:

    (1). 客户端的问题;

    (2). 一个线程中不能建立连接的同时还发送数据。

    那怎么办呢?改客户端代码呗,子线程再建子线程用于发送数据。

    下面进入正题,首先给出我开发环境:

    IDE: Android Studio 2.3.3

    API: android-25(Android 7.1)

    JDK: 1.8.0

    建立 AS 工程时,我设置的最低SDK支持版本是 24(Android 7.0),我的手机是基于 Android 7.1 的系统,是直接用手机连接电脑调试的,没有用虚拟设备。

    原始代码是从网上某个博主哪里找来的,不过客户端已经被我改的面目全非了,除了按钮的名字,差不多看不到原来的样子了。。服务端倒是没怎么改。

    多线程我是用线程池实现的,因此不了解线程池的小伙伴自己先去了解一下,相比于普通的多线程,线程池有很多优点(避免重复开辟新线程,节省资源啥的...)。多线程有很多坑,比如某些东西不能放在某些地方,我也是踩了很多,一遍遍试过来的,程序里面我也注释了一点。

    1. 程序结构、功能、界面

    程序开始运行,设置UI界面,此时运行的是主线程。

    然后,主线程新建一个线程,用于建立 socket 连接。连接成功后最上面的地方会显示服务器端发送的消息(类似于“服务器/192.168.0.101加入;此时总连接:1(服务器发送)”),如果显示的是“TCP-IP”(即项目名称),就说明没连上。

    然后在新线程中监听按钮,当按钮被按下时,再新开子线程用于发送数据。

    界面布局如下:

    2. 代码

    一共在 AS 下建立了两个工程,用两个 AS 窗口分别显示。

    一个工程名为 Tcp-Ip,这个工程为客户端程序,运行在手机端,是一个正常的 Android 工程。工程目录如图:

    另一个工程名为 Tcp-Ip-Server,这个工程为服务端程序,运行在PC端,是一个空的 Android 工程,里面添加了名为 TCPServer 的模块(Java Library型的module),运行的时候也是只运行这个模块(详情自己百度,就是在 Android Studio 下开发普通java程序)。工程目录如图:

     

    客户端

     AndroidManifest.xml,可以参考一下,主要是添加权限:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.gao.tcp_ip">
    
        <uses-permission android:name="android.permission.INTERNET"/>
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".SocketActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>

    SocketActivity.java,主程序:

    package com.example.gao.tcp_ip;
    
    import android.os.Handler;
    import android.os.Message;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    import android.widget.EditText;
    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.Socket;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class SocketActivity extends AppCompatActivity {
    
        public TextView tv_msg = null;
        public EditText ed_msg = null;
        public Button btn_send = null;
        public Button btn_break = null;
        public String content = "";
        public String temp = "";
        private ExecutorService SocketService = Executors.newSingleThreadExecutor(); //创建单线程池,用于套接字连接
    
        // 接收线程发送过来信息,并用TextView显示
        public Handler mHandler = new Handler() {
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                tv_msg.setText(content);
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_socket);
    
            tv_msg = (TextView) findViewById(R.id.tv_contents);
            ed_msg = (EditText) findViewById(R.id.et_content);
            btn_send = (Button) findViewById(R.id.bt_sender);
            btn_break = (Button) findViewById(R.id.bt_break);
    
            SocketService.execute(socketService);  // 在线程池中开启新线程socketService
        }
    
        /// 定义Runnable接口变量socketService完成的功能
        Runnable socketService = new Runnable(){
            // Socket变量不能定义在run中,否则不能在按钮响应函数中调用,BufferedReader、PrintWriter好像可以
            private Socket socket = null;
            private BufferedReader in = null;
            private PrintWriter out = null;
            private static final String HOST = "192.168.0.100";
            private static final int PORT = 6666;
            private String msg;
            private ExecutorService SendService = Executors.newSingleThreadExecutor(); //创建单线程池,用于发送数据
    
            @Override
            public void run(){
                try {
                    //建立套接字连接,获取输入输出流
                    socket = new Socket(HOST, PORT);
                    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
    
                    while (true) {
                        // 通过输入流从服务器获取数据并发送给主线程
                        if (!socket.isClosed() && socket.isConnected() && !socket.isInputShutdown()) {
                            if ((temp = in.readLine()) != null) {
                                content += temp + "
    ";
                                mHandler.sendMessage(mHandler.obtainMessage());
                            }
                        }
    
                        //获取编辑框内容并开启新线程发送
                        btn_send.setOnClickListener( new OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                msg = ed_msg.getText().toString();              //获取发送数据
                                ed_msg.getText().clear();
                                if (socket.isConnected() && !socket.isOutputShutdown()) {
                                    if(out != null) {
                                        SendService.execute(sendService);       //开启发送数据线程
                                    }
                                }
                            }
                        });
    
                        // 开启新线程发送"exit"信号
                        btn_break.setOnClickListener( new OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                msg = "exit";                                   //定义发送数据
                                if (socket.isConnected() && !socket.isOutputShutdown()) {
                                    if(out != null) {
                                        SendService.execute(sendService);       //开启发送数据线程
                                    }
                                }
                            }
                        });
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            //定义发送消息接口
            Runnable sendService = new Runnable(){
                @Override
                public void run() {
                    out.println(msg);   //通过输出流发送数据
                }
            };
        };
    
        public void exit(View view){
            finish();
        }
    }

    activity_socket.xml,布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.example.gao.tcp_ip.SocketActivity">
    
        <TextView
            android:id="@+id/tv_contents"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:text="@string/app_name"
            tools:layout_constraintTop_creator="1"
            tools:layout_constraintRight_creator="1"
            tools:layout_constraintBottom_creator="1"
            app:layout_constraintBottom_toTopOf="@+id/et_content"
            android:layout_marginStart="2dp"
            android:layout_marginEnd="2dp"
            app:layout_constraintRight_toRightOf="parent"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0" />
    
        <EditText
            android:id="@+id/et_content"
            android:layout_width="0dp"
            android:layout_height="54dp"
            android:inputType="text"
            tools:layout_constraintTop_creator="1"
            tools:layout_constraintRight_creator="1"
            android:layout_marginStart="2dp"
            android:layout_marginEnd="2dp"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginTop="194dp"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            tools:layout_constraintTop_creator="1"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintLeft_toLeftOf="@+id/tv_contents"
            app:layout_constraintTop_toTopOf="parent"
            android:id="@+id/linearLayout">
    
        </LinearLayout>
    
        <Button
            android:id="@+id/bt_sender"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="发送"
            android:layout_marginStart="23dp"
            tools:layout_constraintTop_creator="1"
            app:layout_constraintTop_toBottomOf="@+id/et_content"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintLeft_toLeftOf="@+id/et_content" />
    
        <Button
            android:id="@+id/bt_break"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="断开连接"
            android:layout_marginStart="35dp"
            tools:layout_constraintTop_creator="1"
            app:layout_constraintTop_toBottomOf="@+id/et_content"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintLeft_toRightOf="@+id/bt_sender" />
    
        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="退出"
            android:onClick="exit"
            android:layout_marginEnd="13dp"
            tools:layout_constraintTop_creator="1"
            tools:layout_constraintRight_creator="1"
            app:layout_constraintRight_toRightOf="@+id/et_content"
            app:layout_constraintTop_toBottomOf="@+id/et_content" />
    
    </android.support.constraint.ConstraintLayout>

     

    服务端

    相比于客户端,服务器端的代码我没有怎么改(因为它能直接用),就是差不多直接用的别人的博客里的,具体哪个忘了。。。。

    由于运行的是 java 模块(MainActivity和activity_main.xml压根就没用),所以我只贴模块的代码。

    TCPServer.java:

    package com.example;
    
    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.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class TCPServer {
        private static final int PORT = 6666;
        private List<Socket> mList = new ArrayList<>();
        private ServerSocket server = null;
        private ExecutorService mExecutorService = null; // 创建一个线程池
    
        public static void main(String[] args) {
            new TCPServer();
        }
    
        public TCPServer() {
            try {
                server = new ServerSocket(PORT);
                mExecutorService = Executors.newCachedThreadPool(); // 实例化一个线程池
                System.out.println("服务器已启动,等待加入...");
                Socket client = null;
                while (true) {
                    client = server.accept();
                    // 把客户端放入客户端集合中
                    mList.add(client);
                    mExecutorService.execute(new Service(client)); // 开启一个新线程
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /// 扩展Runnable接口
        class Service implements Runnable {
            private Socket socket;
            private BufferedReader in = null;
            private String msg = " ";
    
            public Service(Socket socket) {
                this.socket = socket;
                try {
                    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    // 客户端只要一连到服务器,便向客户端发送下面的信息。
                    msg = "服务器" + this.socket.getInetAddress() + "加入;此时总连接:" + mList.size() + "(服务器发送)";
                    this.sendmsg();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            @Override
            public void run() {
                try {
                    while (true) {
                        if ((msg = in.readLine()) != null) {
                            // 当客户端发送的信息为:exit时,关闭连接
                            if (msg.trim().equals("exit")) {
                                System.out.println("GAME OVER");
                                mList.remove(socket);
                                in.close();
                                msg = "服务器" + socket.getInetAddress() + "退出;此时总连接:" + mList.size() + "(服务器发送)";
                                socket.close();
                                this.sendmsg();
                                break;
                            } else {
                                // 接收客户端发过来的信息msg,然后发送给客户端。
                                msg = socket.getInetAddress() + "" + msg + "(服务器发送)";
                                this.sendmsg();
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            /**
             * 循环遍历客户端集合,给每个客户端都发送信息。
             */
            public void sendmsg() {
                // 在服务器上打印
                System.out.println(msg);
                // 遍历打印到每个客户端上
                int num = mList.size();
                for (int i = 0; i < num; i++) {
                    Socket mSocket = mList.get(i);
    
                    PrintWriter out = null;
                    try {
                        out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream())), true);
                        out.println(msg);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    当客户端连接并发送数据时,运行效果如下:

    最后的最后,再来愉快(juewang)地吐槽一下吧,用 Qt 或者 C++ 库很简单就能实现的 socket,为啥 Android 就这么费劲呢???这么一个简单的功能,要搞这么多线程,让整个程序看起来很复杂。。。。哎

    目前看来,c++ 的 socket 客户端只能跟 c++ 的服务端通信,java 的 socket 客户端只能跟 java 的服务端通信,跨越语言的通信,,,,,我是不行了,。。。

  • 相关阅读:
    windows下php+apache+mysql环境搭建
    sql中的case when
    zend_db连接mysql(附完整代码)(转)
    自定加载的简单实例
    Zend Framework 留言本实战(转)
    C++中虚函数的作用是什么?它应该怎么用呢?(转)
    PHP输入流php://input(转)
    js中===与==区别
    ajax之cache血与泪~~
    js中的string.format
  • 原文地址:https://www.cnblogs.com/hxzkh/p/10564426.html
Copyright © 2011-2022 走看看