安卓Socket通信实例

​ 推荐一个很棒的文章

https://blog.csdn.net/qq7342272/article/details/9698413

1、Socket通信必须知道的地方

1、首先,Socket通信采用TCP模式,客户端需要连接到服务端

2、采用网络,需要打开Internet权限

3、需要合理调用线程,熟悉线程与输入输出流的使用

4、socket服务端和客户端编码方式不一致将有可出现中文乱码问题

5、在收发数据前,必须和对方建立可靠的连接。

  1. 一个TCP连接必须要经过三次“对话”才能建立,其中的过程非常复杂,
  2. 过程:主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;
  3. 主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;
  4. 主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。

6、线程:

  1. 线程是进程中的一个实体,是被系统独立调度和分配的基本单位。
  2. 一个进程可以有多个线程,一个线程必须有一个父进程,线程自己不拥有系统资源,只有运行必须的一些数据结构,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
  3. 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

7、文件输入流与输出流的讲解 :

1、InputStream与OuputStream的区别

​ :slightly_smiling_face:

​ 不管你从磁盘读,从网络读,或者从键盘读,读到内存,就是InputStream。

​ 不管你写到磁盘,写到网络,或者写到屏幕,都是OuputStream。

​ Socket通信中也是如此

​ :apple:

2、如何分清InputStream与OuputStream

我们所说的流,都是针对内存说的,比如为什么打印到屏幕上就是System.out.println();而从屏幕等待用户输入的却是System.in呢?因为对于内存来说,把字符串打印到屏幕上是从内存流向屏幕这个显示器的,也就是输出,而从屏幕等待用户输入呢?就是等待键盘将字符输入到内存中。

所以,你根本就不用死记硬背,当你遇到IO的时候,就想两件事,第一,我的内存是中心,第二看看流的方向(矢量)!

那我访问网络,看网页是什么呢 网络--------------->内存 是 in 因为我们访问页面是要抓取该页面得一个html文件,那我要是在网络上输入帐号密码登陆呢? 是不是内存的东西要写到该服务器上呢,所以当然是out了!

3、Socket中的Recieve与Send

​ 同样socket编程用到更多的IO,这里分别用Server(服务器端)和Client(客户端)来说明

Server: 遇到请求,网络----->内存 IN 服务器应答, 内存------->网络 OUT
-----------------------------------------------------------------
Client: 请求服务, 内存----->网络 OUT 服务器应答, 网络------->内存 IN

注:

当服务端需要读取本地ip时可以使用WifiInfo的getIpAddress()方法,服务端部分已集成,可直接初始化调用

2、思路分析:

1、分清Client和Server:

  1. Client即客户端,用于与服务端进行交互,在本实例中用于与服务端一对一聊天。
  2. Server即服务端,用于启动服务,供客户端通过指定方式连接(Socket中使用IP地址和端口号)。
  3. Server中可以指定端口号,在Client中需要输入服务端的IP地址和端口号才可以进行握手。
  4. 无论在什么地方的Server总是先运行,Client后运行。
  5. Client和Server的初始化方式不一样,出错将导致无法握手!

2、功能模块化

  1. 不同的功能使用不同的线程,尽量降低各模块之间的耦合性
  2. 对于处理类似于控制TextView中的值时,需要专门提供一个线程来处理(Handler的消息机制),避免程序无故报错导致跑飞
  3. 在线程中需要操作控件时巧妙使用消息传递的中间体:Message,可以完美避免未知错误的产生
  4. 在 Java 中所有数据都是使用流读写的。流是一组有序的数据序列,将数据从一个地方带到另一个地方。根据数据流向的不同,可以分为输入(Input)流和输出(Output)流两种。
  5. 在流的使用中必须要使用byte数组!
  6. 为了保证在握手成功之后正确接收到信息需要在握手过程中传入接收部分功能
  7. 要懂得在握手断开后处理好异常而不至于报错,同时还可以在程序中直接反馈相关信息,这是最主要的

3、题外话

  1. 如果消息过长可以使用scrollbars
  2. scrollbars在布局中创建,并且需要在活动中初始化

3、下面代码中的错误代码展示与说明:

/*        信息:                                说明:                                  解决办法:                */
//        我们已经是好友了,开始聊天吧!            -->握手(连接)成功                        -->可收发
//        您未添加对方为好友,无法聊天!            -->未握手,但是程序已结束                -->需重新运行并握手
//        消息发送失败,请查看系统消息!            -->此时未握手或已断开                     -->需重新运行并握手
//        对方下线,聊天结束!                     -->双方中的一方主动断开                    -->需重新运行并握手

3、分步解析(根据实际调用顺序)

Manifest联网请求:

<!--Client部分-->
<uses-permission android:name="android.permission.INTERNET"/>

<!--Server部分-->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

1、初始化Socket

初始化Socket时需要在一个线程中创建

//    Client
Socket socket=new Socket("ip",port);

//    Server
serverSocket = new ServerSocket(5000);
Socket socket = serverSocket.accept();

2、运行Runnable对象(开启线程)

new Thread(xxx).start();

这时候已经初始化完毕,运行成功了

3、读取主要方法

此方法需配合线程使用,否则只会读取一次,建议创建一个新的线程继承类

InputStream inputStream = socket.getInputStream();    //从Socket中获取输入流
byte[] buffer = new byte[xxx];                        //初始化byte数组,用于存储输入流中的值(收信息)
inputStream.read(buffer);                            //输入流中的数据读入byte数组中
String str = new String(buffer,"UTF-8").trim();        //将byte数组转化为字符串

4、读取完毕后需要对控件操作或者判断最好将str传送给Message

Message自我介绍:Handler接收与处理的消息对象

//    Message message = new Message();    //效果相同
Message message = Message.obtain();        //Google推荐
message.obj=str;
handler.sendMessage(message);

5、显示(Handler+handleMessage内处理传入的消息)

Handler自我介绍:作用就是发送与处理信息

​ 如果希望我正常工作,在当前线程中要有一个Looper对象

//提示:如此…………,这般………………

Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
              /*switch语句或其他判断方式,最好message传值使用*/      
        }
    };

6、发送部分构建

//初始化byte数组用于存放准备发送的信息
byte [] sendBuffer = "发送了一条信息".getBytes("UTF-8");
//获得socket的文件输出流
OutputStream outputStream = socket.getOutputStream();
//将sendBuffer写入输出流中,用于输出
outputStream.write(sendBuffer);
//发送成功!

7、测试Socket Client是否成功的最小Socket Server

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

public class TestServer {
    //在Andorid Studio中新建类后直接运行此类,并非Activity
    public static void main(String[] args) {

        try {
            ServerSocket serverSocket = new ServerSocket(5000);
            System.out.println("服务器已连接");
            while(true){
                System.out.println("123");
                Socket socket = serverSocket.accept();
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write("你好,我是服务器\n".getBytes("utf-8"));
                outputStream.close();
                socket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

8、在Server中显示服务端IP的API,可在活动中调用

package com.Suk.Lee;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;

public class NetWorkUtils {


    /**
     * 检查网络是否可用
     *
     * @param paramContext
     * @return
     */
    public static boolean checkEnable(Context paramContext) {
        boolean i = false;
//        "connectivity"
        NetworkInfo localNetworkInfo = ((ConnectivityManager) paramContext
                .getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
        if ((localNetworkInfo != null) && (localNetworkInfo.isAvailable()))
            return true;
        return false;
    }

    /**
     * 将ip的整数形式转换成ip形式
     *
     * @param ipInt
     * @return
     */
    public static String int2ip(int ipInt) {
        StringBuilder sb = new StringBuilder();
        sb.append(ipInt & 0xFF).append(".");
        sb.append((ipInt >> 8) & 0xFF).append(".");
        sb.append((ipInt >> 16) & 0xFF).append(".");
        sb.append((ipInt >> 24) & 0xFF);
        return sb.toString();
    }

    /**
     * 获取当前ip地址
     *
     * @param context
     * @return
     */
    public static String getLocalIpAddress(Context context) {
        try {
            WifiManager wifiManager = (WifiManager) context
                    .getSystemService(Context.WIFI_SERVICE);
            WifiInfo wifiInfo = wifiManager.getConnectionInfo();
            int i = wifiInfo.getIpAddress();
            return int2ip(i);
        } catch (Exception ex) {
            return " 获取IP出错鸟!!!!请保证是WIFI,或者请重新打开网络!\n" + ex.getMessage();
        }
        // return null;
    }
}

9、最后一句,有话要说

所述的发送与连接一样,都需要线程支持,

所述的三个方法:连接,接收,发送都需要相关的Thread支持,

例如Recieve采用的是线程继承类,

而Send与Connect将都用于new Thread().start的启动

以上将为Socket实例,但不仅仅是最佳实例,可做参考,有更好的请自行更改

4、具体实例

消息过长怎么办?:smile_cat:

消息过长时考虑在xml的(消息框:Textview)中加入scrollbars

<TextView
 ...
    android:scrollbars="vertical"/>

MainActivity中设置MovemenrMethod:

(Textview对象).setMovementMethod(ScrollingMovementMethod.getInstance());

Manifest联网请求:

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

Server部分的连接:

Runnable Connect = new Runnable() {
        @Override
        public void run() {
            try {
                serverSocket = new ServerSocket(5000);
                socket = serverSocket.accept();
                isConnect = true;
                Recieve recieve = new Recieve(socket);
                recieve.start();
                Message message=Message.obtain();
                message.what=2;
                message.obj="我们已经是好友了,开始聊天吧!";
                handler.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    };

Client部分的连接:

Runnable Connect = new Runnable() {
        @Override
        public void run() {
            try {
                socket = new Socket(ip, 5000);
                isConnect = true;
                Recieve recieve = new Recieve(socket);
                recieve.start();
                Message message=Message.obtain();
                message.what=2;
                message.obj="我们已经是好友了,开始聊天吧!";
                handler.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    };

Handler:

Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 0:
                    chatitem.append("\n你:"+msg.obj.toString());
                    edit.setText("");
                    System.out.println("\n你:"+msg.obj.toString());
                    break;
                case 1:
                    chatitem.append("\n接收到的信息:" + msg.obj.toString());
                    System.out.println("接收到的信息:" + msg.obj.toString());
                    break;
                case 2:
                    error.setText(msg.obj.toString());
                    break;
            }

        }
    };

构建接收函数(Receive):

public class Recieve extends Thread {
        private InputStream inputStream;
        Recieve(Socket socket) {
            try {
                inputStream = socket.getInputStream();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void run() {
            super.run();
            while (isConnect) {
                try {
                    byte[] buffer = new byte[512];
                    inputStream.read(buffer);
                    String str = new String(buffer, StandardCharsets.UTF_8).trim();
                    if (!str.trim().equals("")) {
                        Message message = Message.obtain();
                        message.what = 1;
                        message.obj = str;
                        handler.sendMessage(message);
                    } else {
                        Message message = Message.obtain();
                        message.what = 2;
                        message.obj = "对方下线,聊天结束!";
                        handler.sendMessage(message);
                        isConnect = false;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

构建发送函数(Send):

Runnable Send=new Runnable() {
        @Override
        public void run() {
            byte [] sendBuffer=null;
            
            //可替代为接收输入框的字符串.getBytes
            sendBuffer="发送了一条信息".getBytes("UTF-8");
            
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(sendBuffer);
            Message message = Message.obtain();
            message.what=0;
            message.obj = edit.getText().toString();
            handler.sendMessage(message);
        }
    };

开始方法:

//连接方法
new Thread(Connect).start();
//发送方法
//btn.setOnClickListener:new Thread(Send).start();
new Thread(Send).start();

引用部分

client -- server发送过程中,涉及的输入流输出流:

http://blog.csdn.net/dlwh_123/article/details/35982015 (良心好文)