038 Android网络编程-TCP

作者: 凤邪摩羯 | 来源:发表于2021-06-23 09:35 被阅读0次

IP协议属于网络层,TCP、UDP协议属于传输层。
IP协议是TCP/IP协议族的动力,它为上层协议提供无状态、无连接、不可靠的服务。
TCP协议是面向连接的传输层协议,提供一种面向连接的、可靠的字节流服务。
UDP协议是面向无连接的传输层协议,提供面向事务的简单不可靠信息传输服务。

数据报文

在不同层传输的数据单位名称不同,在网络层传输的叫数据报,在传输层传输的叫报文段。

IP数据报

IP数据报格式如下图:

image

各个字段的详细说明:

名称 长度 说明
版本 4bit IP协议的版本,目前的IP协议版本号为4,下一代IP协议版本号为6
首部长度 4bit IP报头的长度,最大长度60字节(15*4),分为固定部分的长度(20字节)和可变部分的长度
服务类型 8bit Type Of Service
总长度 16bit IP报文的总长度。数据报的最大长度为 65535 字节
标识 16bit 它是一个计数器,用来产生数据报的标识,当IP报文长度超过传输网络的MTU(最大传输单元)时必须分片,此标识表示同一个数据报的分片。
标志 3bit R、DF、MF三位,目前只有后两位有效。DF位:为1表示不分片,为0表示分片。MF:为1表示“更多的片”,为0表示这是最后一片。
片偏移 13bit 本分片在原先数据报文中相对首位的偏移位。片偏移以8个字节为偏移单位。
生存时间 8bit TTL (Time To Live)表示数据报在网络中的寿命,其单位为秒。在目前的实际应用中,常以“跳”为单位。
协议 8bit 指出IP报文携带的数据使用的哪种协议,以便目的主机的IP层能知道要将数据报上交到哪个进程。TCP的协议号为6,UDP的协议号为17。ICMP的协议号为1,IGMP的协议号为2.
首部校验和 16bit 计算IP头部的校验和,检查IP报头的完整性。
源地址 32bit 标识IP数据报的源端设备。
目的地址 32bit 标识IP数据报的目的地址。
可选字段 长度可变 1~40 字节,用于增加IP数据报的控制功能。
填充 保证IP首部长度是4字节的整倍数

TCP报文

image

名称 长度 说明
源端口 16bit 数据发送方的端口号
目的端口 16bit 数据接受方的端口号
序号 32bit 本数据报文中的的第一个字节的序号
(在数据流中每个字节都对应一个序号)
确认号 32bit 希望收到的下一个数据报文中的第一个字节的序号
数据偏移 4bit 表示本报文数据段距离报文段有多远
保留字段 6bit 保留为今后使用,但目前应置为0
紧急比特URG 当值为1时表示次报文段中有需要紧急处理
确认比特ACK 值为1时确认号有效,值为0时确认号无效
复位比特RST 值为1时表示TCP连接存在严重的错误,需要重新进行连接
同步比特SYN 值为1表示这是一个连接请求或连接接受报文
终止比特FIN 值为1表示要发送的数据报已经发送完毕,需要释放传送连接
窗口 16bit TCP连接的一端根据缓存空间的大小来确定自己接受窗口的大小
限制发送放的窗口上限
检验和 16bit 用来检验首部和数据两部分的正确性
紧急指针字段 16bit 紧急指针指出在本报文段中的紧急数据的最后一个字节的序号
选项字段 长度可变 TCP 首部可以有多达40字节的可选信息,
用于把附加信息传递给终点,或用来对齐其它选项

UDP报文

相对于TCP报文,UDP报文简单了很多。

image
名称 长度 说明
源端口 16bit 数据发送方的端口号
目的端口 16bit 数据接受方的端口号
包长度 16bit UDP首部的长度和数据的长度之和。单位为字节
校验和 16bit 用来检验首部和数据两部分的正确性

TCP三次握手和四次挥手

TCP用三次握手来创建连接,使用四次分手来释放连接。

[图片上传中...(image-f38b3b-1624411076651-0)]

三次握手

三次握手的目的是同步连接双方的序列号和确认号并交换TCP窗口大小的信息。
握手过程:

  1. 第一次握手:建立连接,客户端先发送连接请求报文,将SYN设置为1,Sequence Number为x。客户端进入SYN+SEND状态,等待服务器确认。
  1. 第二次握手:服务器收到SYN报文。服务器收到客户端的SYN报文,需要对这个SYN报文进行确认,设置Acknowledgment Number为x+1(Sequence+1);同时,自己还要送法SYN消息,将SYN位置为1,Sequence Number为y;服务器将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN+RECV状态。
  2. 第三次握手:客户端收到服务器的 SYN+ACK报文段。然后将Acknowlegment Number设为y+1,向服务器发送ACK报文段,这个报文段发送完毕后,客户端端服务器都进入ESTABLISHED状态,完成TCP三次握手。

完成了三次握手,客户端和服务器就可以开始传送数据了。

四次挥手

当客户端和服务端传输数据完毕后,需要断开TCP连接。TCP断开的过程,就是四次挥手。

  1. 第一次挥手:客户端(也可以是服务器),设置Sequence Number和Acknowledgment Number,向服务器发送一个FIN报文段。此时客户端进入FIN_WAIT_1状态;这表示客户端没有数据发送给主机了。
  1. 第二次挥手:服务器收到客户端发来的FIN报文段,向客户端回一个ACK报文段,Acknowledgement Number为Sequence Number加1;客户端进入FIN_WAIT_2状态,服务器进入CLOSE_WAIT状态;服务器告诉客户端,我同意你的"关闭"请求。
  2. 第三次挥手:服务器向客户端发送FIN报文段,请求关闭连接,同时服务器进入LAST_ACK状态。
  3. 第四次挥手:客户端收到服务器发送的FIN报文段,向主机发送ACK报文段,然后客户端进入TIME_WAIT状态,服务器收到客户端的ACK报文段以后,就关闭连接,此时,客户端等待2MSL后一次没有到收到回复,则证明服务端已正常关闭,那好,客户端也可以关闭连接了。

TCP三次握手的必要性

防止服务器端因接收了早已失效的连接请求报文,从而一直等待客户端请求,最终导致形成死锁、浪费资源。

TCP四次挥手的必要性

为了保证通信双方都能通知对方,需释放、断开连接。

为什么客户端关闭连接前要等待2MSL时间

MSL: 最大报文段生存时间

四个报文发送完毕后,就可以直接进入CLOSE状态了,但是有可能网络是不可靠的,一切都可能发生,比如有可能最后一个ACK丢失。所以TIME_WAIT状态是用来重发可能丢失的ACK报文。展开具体来讲:

  • 为了保证客户端发送的最后1个连接释放确认报文 能到达服务器,从而使得服务器能正常释放连接。
  • 防止早已失效的连接请求报文,出现在本连接中。客户端发送了最后1个连接释放请求确认报文后,再经过2MSL时间,则可使本连接持续时间内所产生的所有报文段都从网络中消失。

TCP、UDP比较

TCP UDP
可靠性 可靠 不可靠
连接性 面向连接 无连接
报文 面向字节流 面向报文
效率 低效 高效
双工性 全双工 一对一,一对多,多对一,多对多支持多播和广播
流量控制 滑动窗口机制
拥塞控制 慢开始/拥塞避免快重传/快恢复
传输速度
应用场景 效率要求相对低,准确要求相对高。要求有连接的场景 效率要求相对高,准确要求相对低
应用 SMTP,TELNET,HTTP,FTP DNS,RIP,NFS,SNMP,IP电话,流媒体

TCP使用

TCP服务器

常用API:

ServerSocket
new ServerSocket(int port) —— 创建监听端口号为port的ServerSocket
getLocalPort() —— 获取本地端口,即Socket监听的端口
setSoTimeout(int timeout) —— 设置accept()的连接超时时间
accept() —— 等待连接。返回客户端的Socket实例对象。若设置了超时,连接超时将抛异常SocketTimeoutException,否则阻塞等待
isClosed() —— 连接是否关闭
close() —— 关闭连接
Socket
setSoTimeout(int timeout) —— 设置read()读取流的超时时间
getInetAddress().getHostAddress() —— 获取客户端的主机IP地址
getPort() —— 获取客户端与本服务器连接端口
getLocalPort() —— 获取本地端口,与serverSocket.getLocalPort()获取的端口一致
getInputStream() —— 获取输入流,用于接收客户端发送过来的信息。一般使用read(byte[] data)来读取,此方法也属于阻塞式,但若设置了读取流的超时时间,超时将抛异常SocketTimeoutException
getOutputStream() —— 获取输出流,用户给客户端发送信息

这里是一个简单的服务器实例,只实现一次请求,然后响应一次即完毕:

private void startTCPServer() {
    final int port = 8989;

    new Thread(new Runnable() {
        @Override
        public void run() {
            ServerSocket server = null;
            try {
                // 1、创建ServerSocket服务器套接字
                server = new ServerSocket(port);
                // 设置连接超时时间,不设置,则是一直阻塞等待
                server.setSoTimeout(8000);

                // 2、等待被连接。在连接超时时间内连接有效,超时则抛异常,
                Socket client = server.accept();
                logD("connected...");
                // 设置读取流的超时时间,不设置,则是一直阻塞读取
                client.setSoTimeout(5000);

                // 3、获取输入流和输出流
                InputStream inputStream = client.getInputStream();
                OutputStream outputStream = client.getOutputStream();

                // 4、读取数据
                byte[] buf = new byte[1024];
                int len = inputStream.read(buf);
                String receData = new String(buf, 0, len, Charset.forName("UTF-8"));
                logD("received data from client: " + receData);

                // 5、发送响应数据
                byte[] responseBuf = "Hi, I am Server".getBytes(Charset.forName("UTF-8"));
                outputStream.write(responseBuf, 0, responseBuf.length);

            } catch (IOException e) {
                logD("Exception:" + e.toString());
                e.printStackTrace();
            } finally {
                if (server != null) {
                    try {
                        server.close();
                        server = null;
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

            }
        }
    }).start();
}

TCP客户端

常用API:

Socket
new Socket(String host, int port) —— 创建连接。在90秒内未连接主机,会抛异常ConnectException。可以缩短连接超时时间,使用connect方法
new Socket() —— 创建一个套接字。主要用于connect().
connect(SocketAddress socketAddress, int timeout); —— 开始连接给定的套接字地址,并设置连接超时时间。SocketAddress中封装了主机地址与端口。
setSoTimeout(int timeout) —— 设置读取流的超时时间。必须在getInputStream()与getOutputStream()之前设置才有效
isConnected() —— 是否连接上,返回布尔值
getOutputStream() —— 获取输出流
getInputStream() —— 获取输入流
shutdownOutput() —— 关闭输入流。属于半关闭,而close()属于全关闭
close() —— 关闭连接
  • SocketAddress
    new InetSocketAddress(host, port) —— SocketAddress是抽象类,使用SocketAddress子类来实例化。把主机IP地址与端口封装进去。
    客户端代码也是最简单的实例,一发一收即结束。
private void startTCPClient() {
        final String host = "192.168.1.214";
        final int port = 8989;

        new Thread(new Runnable() {
            @Override
            public void run() {
                Socket socket = null;
                try {
                    // 1、创建连接
                    socket = new Socket(host, port);
                    if (socket.isConnected()) {
                        logD("connect to Server success");
                    }

                    // 2、设置读流的超时时间
                    socket.setSoTimeout(8000);

                    // 3、获取输出流与输入流
                    OutputStream outputStream = socket.getOutputStream();
                    InputStream inputStream = socket.getInputStream();

                    // 4、发送信息
                    byte[] sendData = "Hello, I am client".getBytes(Charset.forName("UTF-8"));
                    outputStream.write(sendData, 0, sendData.length);
                    outputStream.flush();

                    // 5、接收信息
                    byte[] buf = new byte[1024];
                    int len = inputStream.read(buf);
                    String receData = new String(buf, 0, len, Charset.forName("UTF-8"));
                    logD(receData);

                } catch (IOException e) {
                    e.printStackTrace();
                    logD(e.toString());
                } finally {
                    if (socket != null) {
                        try {
                            socket.close();
                            socket = null;
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

TCP总结

读取流阻塞的处理

因为上面的demo中,都是假设对方发送的数据长度小于等于1024。可是现实中数据的长度一般都是未知的,很多人用读取文件的方法来读取,如:

byte[] buf = new byte[1024];
StringBuilder rece = new StringBuilder();
int len;
while ((len = inputStream.read(buf)) != -1) {
    String part = new String(buf, 0, len);
    rece.append(part);
}
String receData = rece.toString();

这样,看上去很好,但实际socket的流与文件流不同,文件读到末尾,有一个结束标记。而socket的流只有在关闭流的时候,才会有被告知结束。这里的结束就是read()返回的长度等于-1。
TCP通信时,一般是不会立马关闭,而read()又是阻塞式方法,所以会导致程序一直停留在这里while ((len = inputStream.read(buf)) != -1)。

首先,有两种不太靠谱的解决方法:

  1. 不要用-1作为判断条件,而用>0。但我这结果都一样,不管你信不信,反正我信了
  2. 使用BufferedReader的readLine()去读取数据。这是读行,我的数据里没有换行,怎么读?传文件很好,但绝大多数情况下不适合。
BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream));
String receData = bufReader.readLine();

稍微靠谱点的方法:
使用while(inputStream.available() > 0),先进行判断是否有数据可读,有就在循环内部读取。这个方法不是阻塞式,如果对方已经把数据发送出来了,这时可以正常读取;如果对方慢了一点,这里就直接执行下去了,数据的读取就被跳过了。当然也可以每次都先等待一段时间再去查。

目前没有找到特别完美又通用的解决方法(如果您有,麻烦分享下),但总有适合的,以下是个人总结的几种解决方法:

  1. 制定协议,添加报文头:报文总数据长度 + 是否继续保持链接 + 当前报文的序号,本报文就根据此长度来判断是否接受完毕;
  2. 设置读入超时时间。如果后面的读取后面的程序还要执行,那就只对读取进行异常处理;
  3. 发送端发送完毕后关闭连接。socket.shutdownOutput()和socket.close()都可以,前者是半关闭,后者是全关闭。前者安全点;
  4. 读取到某些字符后,当成结束。如读到byebye就不读了;
  5. 让每次传输的数据长度都不等于缓冲区buffer大小的整数倍。接收到数据后,判断长度是否为buffer长度。是,则说明还有数据要读;否,则说明已结束;
  6. 用一个足够大的缓冲区buffer来一次性装完所有的数据。

流的处理

个人还是喜欢用byte,可能因为以前使用C的原因,再加上项目中也是字节数据+字符数据。很多情况下,我们都是字符串数据或对象实例之类的,可以用封装类进行处理,如BufferedWriter/BufferedReader、ObjectInputStream/ObjectOutputStream、DataInputStream/DataOutputStream与PrintWriter。

对象流就不写demo了,需注意的是:

  • 被读写的对象必须实现Serializable;
  • 读写顺序必须一致。

DataInputStream+DataOutputStream:

// 发送
DataOutputStream dataOS = new DataOutputStream(outputStream);
dataOS.writeUTF("Hi你好啊, I am Server");
dataOS.flush();
dataOS.close(); // 如果发送后不再接收或发送,就可以关闭,否则不要关闭。因为这样也会把socket关闭,导致无法再接收或发送了,并抛SocketException异常。若不再发送,只接收,可用socket.shutdownOutput();

// 接收
DataInputStream dataIS = new DataInputStream(inputStream);
String receData = dataIS.readUTF();
logD("From Client:" + receData);

PrintWriter + BufferedReader:

// 发送
// 使用带自动flush刷新的构造函数,自动刷新仅对这三个方法有效:println, printf, format
PrintWriter printWriter = new PrintWriter(outputStream, true);
printWriter.format("First info from client = %s", host);
printWriter.printf("Second info: Android");
socket.shutdownOutput();

// 接收
// 这里可以一次性把上面两次的写入流读出来。
BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream));
char[] buffer = new char[1024];
int length;
if ((length = bufReader.read(buffer)) != -1) {
    String receData = new String(buffer, 0, length);
    logD("From Client:" + receData);
}

TCP心跳保活

心跳是双方约定好保活时间,在此保活时间内告诉对方自己还在线,不要关闭连接。对方在保活时间内没有收到保活信息,就会关闭连接。
正常情况下,TCP有默认的保活时间,为2小时,可在客户端开启保活功能socket.setKeepAlive(true);。
但我们并不需要那么长的保活时间,一般10分钟就够了。方法有两个:

反射去修改默认的保活时间;
(参考:http://stackoverflow.com/questions/6565667/how-to-set-the-keepalive-timeout-in-android
在应用层面去定时发送心跳包,实现方法很多,就不多讲了。

实例:TCP服务器与客户端自由通信

这里的自由通信,指的是服务器或客户端可以任意的发送和接收数据,而不需按一发一收,想发就发,任意时刻都能接收数据那种。

原理分析

发送用的是OutputStream,接收用的是InputStream。一发一收制,发送和接收的都是顺序写到代码中的。现在要自由,无非就是分开来,发送只管发送,接收只管接收。
发送,好处理,需要发送的时候发送就行。
接收,好像没那么容易,因为对方无论何时发送数据,我们必须接收。这里开了个子线程在循环读流。又由于读流是阻塞式,使用的处理方法是上面的第2种,设置读流超时时间。

代码实现

代码包含服务器类SimpleTCPServer与客户端类SimpleTCPClient,及各自的demo。这两个类可以当做简单的工具类使用。
两个工具类对接收的数据使用了不同的处理方法,SimpleTCPServer要求传入Handler进行处理。而SimpleTCPClient是要求调用者实现其抽象方法processData(byte[] data),此方法是在子线程中处理,把更多的处理权交给了调用者。

SimpleTCPServer:

public class SimpleTCPServer {
    private static final String TAG = SimpleTCPServer.class.getSimpleName();
    private static final int INPUT_STREAM_READ_TIMEOUT = 300;

    /**
     * 服务器,连接服务器的客户端
     */
    private ServerSocket mServer;
    private List<Socket> mClientList = new ArrayList<>();

    private Handler mHandler;

    public SimpleTCPServer(Handler handler) {
        this.mHandler = handler;
    }

    public void listen(final int port) {
        if (mServer != null && !mServer.isClosed()) {
            close();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mServer = new ServerSocket(port);
                    while (mServer != null && !mServer.isClosed()) {
                        logI("start to accept");
                        Socket client = mServer.accept();
                        if (client.isConnected()) {
                            logI(String.format("accepted from: %s[%d]", client.getInetAddress().getHostAddress(), client.getPort()));
                            mClientList.add(client);
                            new Thread(new ReceiveRunnable(mClientList.size() - 1, client)).start();
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * 接收客户端数据的子线程
     */
    class ReceiveRunnable implements Runnable {
        private int which;
        private Socket mSocket;

        ReceiveRunnable(int which, Socket socket) {
            this.which = which;
            this.mSocket = socket;
        }

        @Override
        public void run() {
            try {
                // 给读取流设置超时时间,否则会一直在read()那阻塞
                mSocket.setSoTimeout(INPUT_STREAM_READ_TIMEOUT);
                InputStream in = mSocket.getInputStream();
                while (mSocket != null && mSocket.isConnected()) {
                    // 读取流
                    byte[] data = new byte[0];
                    byte[] buf = new byte[1024];
                    int len;
                    try {
                        while ((len = in.read(buf)) != -1) {
                            byte[] temp = new byte[data.length + len];
                            System.arraycopy(data, 0, temp, 0, data.length);
                            System.arraycopy(buf, 0, temp, data.length, len);
                            data = temp;
                        }
                    } catch (SocketTimeoutException stExp) {
                        // 只catch,不做任何处理
                        // stExp.printStackTrace();
                    }

                    // 处理流
                    if (data.length != 0) {
                        pushMsgToHandler(which, data);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 推送信息给Handler
     */
    private void pushMsgToHandler(int which, byte[] data) {
        Message message = mHandler.obtainMessage();
        message.what = which;
        message.obj = data;
        mHandler.sendMessage(message);
    }

    /**
     * 发送数据
     */
    public boolean sendData(int which, byte[] bytes) {
        if (which < 0 || which >= mClientList.size()) {
            return false;
        }

        Socket socket = mClientList.get(which);
        if (socket != null && socket.isConnected()) {
            try {
                OutputStream out = socket.getOutputStream();
                out.write(bytes);
                out.flush();
                return true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    public boolean sendData(int which, String data) {
        return sendData(which, data.getBytes(Charset.forName("UTF-8")));
    }

    public void close() {
        if (mServer != null) {
            try {
                mServer.close();
                mServer = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        for (Socket client : mClientList) {
            try {
                client.close();
                client = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        mClientList.clear();
    }

    public Socket getClient(int which) {
        return which < 0 || which >= mClientList.size() ? null : mClientList.get(which);
    }

    public int getClientCount() {
        return mClientList.size();
    }

    private void logI(String msg) {
        Log.i(TAG, msg);
    }
}

SimpleTCPServer demo:

public class ServerActivity extends AppCompatActivity {

    private TextView tv_msg;
    private Button btn_restart;

    private SimpleTCPServer mSimpleTCPServer;

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

        initData();
        initView();
    }

    private void initData() {
        mSimpleTCPServer = new SimpleTCPServer(mHandler);
        mSimpleTCPServer.listen(9000);
    }

    private void initView() {
        tv_msg = (TextView) findViewById(R.id.tv_msg);
        btn_restart = (Button) findViewById(R.id.btn_restart);

        tv_msg.setMovementMethod(ScrollingMovementMethod.getInstance());
        btn_restart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mSimpleTCPServer != null) {
                    for (int i = 0; i < mSimpleTCPServer.getClientCount(); i++) {
                        mSimpleTCPServer.sendData(i, "Push From Sever, 服务器发送数据来了".getBytes(Charset.forName("UTF-8")));
                    }
                }
            }
        });
        btn_restart.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                tv_msg.setText("");
                return true;
            }
        });
    }


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

    private MyHandler mHandler = new MyHandler(this);
    private static class MyHandler extends Handler {
        WeakReference<ServerActivity> refActivity;

        public MyHandler(ServerActivity activity) {
            refActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            ServerActivity activity = refActivity.get();
            int which = msg.what;
            byte[] data = (byte[]) msg.obj;
            String result = new String(data, Charset.forName("UTF-8"));
            activity.tv_msg.append("第" + which + "位访客"
                    + activity.mSimpleTCPServer.getClient(which).getInetAddress().getHostAddress() + ":"
                    + result + "\n");
            activity.mSimpleTCPServer.sendData(msg.what, "Hi你好,I am server".getBytes(Charset.forName("UTF-8")));
        }
    }
}

SimpleTCPClient:

public abstract class SimpleTCPClient {
    private static final String TAG = SimpleTCPClient.class.getSimpleName();
    private static final int CONNECT_TIMEOUT = 5000;
    private static final int INPUT_STREAM_READ_TIMEOUT = 300;

    private Socket mSocket;
    private InputStream mInputStream;
    private OutputStream mOutputStream;

    public SimpleTCPClient() {
    }

    /**
     * 连接主机
     * @param host 主机IP地址
     * @param port 主机端口
     */
    public void connect(final String host, final int port) {
        if (mSocket != null) {
            close();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mSocket = new Socket();
                    SocketAddress socketAddress = new InetSocketAddress(host, port);
                    // 设置连接超时时间
                    mSocket.connect(socketAddress, CONNECT_TIMEOUT);
                    if (mSocket.isConnected()) {
                        logI("connected to host success");
                        // 设置读流超时时间,必须在获取流之前设置
                        mSocket.setSoTimeout(INPUT_STREAM_READ_TIMEOUT);
                        mInputStream = mSocket.getInputStream();
                        mOutputStream = mSocket.getOutputStream();
                        new ReceiveThread().start();
                    } else {
                        mSocket.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * 接收进程
     */
    class ReceiveThread extends Thread {
        @Override
        public void run() {
            super.run();

            while (mSocket != null && mSocket.isConnected() && mInputStream != null) {
                // 读取流
                byte[] data = new byte[0];
                byte[] buf = new byte[1024];
                int len;
                try {
                    while ((len = mInputStream.read(buf)) != -1) {
                        byte[] temp = new byte[data.length + len];
                        System.arraycopy(data, 0, temp, 0, data.length);
                        System.arraycopy(buf, 0, temp, data.length, len);
                        data = temp;
                    }
                } catch (IOException e) {
//                    e.printStackTrace();
                }

                // 处理流
                if (data.length != 0) {
                    processData(data);
                }
            }

        }
    }

    /**
     * process data from received,which is not run in the main thread。
     */
    public abstract void processData(byte[] data);

    /**
     * 发送数据
     */
    public void send(byte[] data) {
        if (mSocket != null && mSocket.isConnected() && mOutputStream != null) {
            try {
                mOutputStream.write(data);
                mOutputStream.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void send(String data) {
        send(data.getBytes(Charset.forName("UTF-8")));
    }

    public void close() {
        if (mSocket != null) {
            try {
                mInputStream.close();
                mOutputStream.close();
                mSocket.close();
                mInputStream = null;
                mOutputStream = null;
                mSocket = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public Socket getSocket() {
        return mSocket;
    }

    private void logI(String msg) {
        Log.i(TAG, msg);
    }
}

SimpleTCPClient demo:

public class ClientActivity extends AppCompatActivity {

    private Button btn_send;
    private TextView tv_msg;
    private SimpleTCPClient mSimpleTCPClient;


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

        initData();
        initView();
    }

    private void initData() {
        mSimpleTCPClient = new SimpleTCPClient() {
            @Override
            public void processData(byte[] data) {
                sendMessageToActivity(new String(data, Charset.forName("UTF-8")));
            }
        };
        mSimpleTCPClient.connect("192.168.1.214", 9000);
    }

    private void initView() {
        tv_msg = (TextView) findViewById(R.id.tv_msg);
        btn_send = (Button) findViewById(R.id.btn_send);

        tv_msg.setMovementMethod(ScrollingMovementMethod.getInstance());
        btn_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mSimpleTCPClient.send("Hello,from client:测试数据来了");
            }
        });
        btn_send.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                tv_msg.setText("");
                return true;
            }
        });
    }

    private void sendMessageToActivity(String msg) {
        Message message = Message.obtain();
        message.obj = msg + "\n";
        mHandler.sendMessage(message);
    }

    private MyHandler mHandler = new MyHandler(this);
    private static class MyHandler extends Handler {
        private WeakReference<ClientActivity> refActivity;

        MyHandler(ClientActivity activity) {
            refActivity = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            ClientActivity activity = refActivity.get();
            activity.tv_msg.append((CharSequence) msg.obj);
        }
    }

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

相关文章

  • 038 Android网络编程-TCP

    IP协议属于网络层,TCP、UDP协议属于传输层。IP协议是TCP/IP协议族的动力,它为上层协议提供无状态、无连...

  • Android 基于TCP/IP的Socket通信

    参考 1、Android网络编程 - TCP/IP协议实践2、Android-TCP客户端的实现3、Android...

  • Android网络编程

    Android网络编程 基于TCP协议的网络通信 TCP/IP通信协议是一种可靠的网络协议,它在通信的两端各建立一...

  • Android 网络编程 目录

    Android 网络编程 目录 Android 网络编程1 Http协议Android 网络编程2 Okhttp缓...

  • 网络基础介绍

    网络编程的两种 TCP socket编程,是网络编程的主流。之所以叫Tcp socket编程,是因为底层是基于Tc...

  • 网络通信

    Android网络编程分为两种:基于Socket和基于Http协议。基于Socket:(1)针对TCP/IP的So...

  • Python TCP编程

    Python网络编程之TCP 一、TCP协议 TCP协议,传输控制协议(Transmission Control ...

  • Linux网络编程篇之ICMP协议分析及ping程序实现

    Linux网络编程系列: Linux网络编程篇之Socket编程预备知识 Linux网络编程篇之TCP协议分析及聊...

  • android中基本框架的使用及分析

    结合《android进阶之光》分析 网络编程部分(之前准备知识为多线程知识) 网络分层理解 TCP的三次握手与四次...

  • Android网络编程-TCP/IP协议

    在Android网络编程-计算机网络基础一文中得知,IP协议属于网络层,TCP、UDP协议属于传输层。IP协议是T...

网友评论

    本文标题:038 Android网络编程-TCP

    本文链接:https://www.haomeiwen.com/subject/pryzoktx.html