Java 网络编程 之 socket 的用法与实现

Java 网络编程 之 socket 的用法与实现

一、概念

TCP

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内 另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。

应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元( MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

JAVA Socket

所谓socket 通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

以J2SDK-1.3为例,Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。

重要的Socket API

java.net.Socket继承于java.lang.Object,有八个构造器,其方法并不多,下面介绍使用最频繁的三个方法,其它方法大家可以见JDK-1.3文档。

  • Accept方法用于产生”阻塞”,直到接受到一个连接,并且返回一个客户端的Socket对象实例。”阻塞”是一个术语,它使程序运行暂时”停留”在这个地方,直到一个会话产生,然后程序继续;通常”阻塞”是由循环产生的。

  • getInputStream方法获得网络连接输入,同时返回一个InputStream对象实例。

  • getOutputStream方法连接的另一端将得到输入,同时返回一个OutputStream对象实例。

注意:其中getInputStream和getOutputStream方法均会产生一个IOException,它必须被捕获,因为它们返回的流对象,通常都会被另一个流对象使用。

二、TCP 编程

服务器端套路

  1. 创建ServerSocket对象,绑定监听端口。

  2. 通过accept()方法监听客户端请求。

  3. 连接建立后,通过输入流读取客户端发送的请求信息。

  4. 通过输出流向客户端发送响应信息。

  5. 关闭响应的资源。

客户端套路

  1. 创建Socket对象,指明需要连接的服务器的地址和端口号。
  2. 连接建立后,通过输出流向服务器发送请求信息。
  3. 通过输入流获取服务器响应的信息。
  4. 关闭相应资源。

多线程实现服务器与多客户端之间通信步骤

  1. 服务器端创建ServerSocket,循环调用accept()等待客户端连接。

  2. 客户端创建一个socket并请求和服务器端连接。

  3. 服务器端接受客户端请求,创建socket与该客户建立专线连接。

  4. 建立连接的两个socket在一个单独的线程上对话。

  5. 服务器端继续等待新的连接。

三、Socket通信基本示例

1、基础模式

这种基础模式,必须掌握,后期对Socket的优化都是在这个基础上的,也是为以后学习NIO做铺垫。

服务端

package org.zpli.socket.server;

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

/**
 * @Description: 简单的socket服务端
 * @author: zpli
 * @Date: 2020/4/16 9:59
 */
public class ServerSocketDemo1 {

    public static void main(String[] args)  {
        // 监听指定的端口
        int port = 55533;
        ServerSocket server = null;
        Socket socket = null;
        InputStream inputStream=null;
        try {
            server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");
            socket = server.accept();
            // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
            inputStream = socket.getInputStream();
            byte[] bytes = new byte[1024];
            int len;
            StringBuilder sb = new StringBuilder();
            while ((len = inputStream.read(bytes)) != -1) {
                //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
                sb.append(new String(bytes, 0, len, "UTF-8"));
            }
            System.out.println("get message from client: " + sb);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                if(null != inputStream){
                    inputStream.close();
                }
                if(null != socket){
                    socket.close();
                }
                if(null != server){
                    server.close();
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

客户端

package org.zpli.socket.client;

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

/**
 * @Description: TODO
 * @author: zpli
 * @Date: 2020/4/16 10:04
 */
public class SocketClient1 {
    public static void main(String args[]) {
        // 要连接的服务端IP地址和端口
        String host = "127.0.0.1";
        int port = 55533;
        Socket socket = null;
        OutputStream outputStream = null;
        try {
            // 与服务端建立连接
            socket = new Socket(host, port);
            // 建立连接后获得输出流
            outputStream = socket.getOutputStream();
            String message = "你好  世界如此美丽";
            outputStream.write(message.getBytes("UTF-8"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != outputStream) {
                    outputStream.close();
                }
                if (null != socket) {
                    socket.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

客户端通过ip和端口,连接到指定的server,然后通过Socket获得输出流,并向其输出内容,服务器会获得消息。最终服务端控制台打印如下:

server将一直等待连接的到来
get message from client: 你好  世界如此美丽

通过这个例子应该掌握并了解:

  • Socket服务端和客户端的基本编程
  • 传输编码统一指定,防止乱码

这个例子做为学习的基本例子,实际开发中会有各种变形,比如客户端在发送完消息后,需要服务端进行处理并返回,如下:

2、双向通信,发送消息并接受消息

服务端

package org.zpli.socket.server;

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

/**
 * @Description: TODO
 * @author: zpli
 * @Date: 2020/4/16 10:12
 */
public class ServerSocketDemo2 {

    public static void main(String[] args) {
        // 监听指定的端口
        int port = 55533;
        ServerSocket server = null;
        Socket socket = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");
            socket = server.accept();
            // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
            inputStream = socket.getInputStream();
            byte[] bytes = new byte[1024];
            int len;
            StringBuilder sb = new StringBuilder();
            while ((len = inputStream.read(bytes)) != -1) {
                //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
                sb.append(new String(bytes, 0, len, "UTF-8"));
            }
            outputStream = socket.getOutputStream();
            outputStream.write("我已经收到客户端发送的数据,这是我的响应结果".getBytes("utf-8"));
            System.out.println("get message from client: " + sb);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
                if(null != outputStream){
                    outputStream.close();
                }
                if (null != socket) {
                    socket.close();
                }
                if (null != server) {
                    server.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

客户端

package org.zpli.socket.client;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @Description: TODO
 * @author: zpli
 * @Date: 2020/4/16 10:15
 */
public class SocketClient2 {

    public static void main(String args[]) {
        // 要连接的服务端IP地址和端口
        String host = "127.0.0.1";
        int port = 55533;
        Socket socket = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            // 与服务端建立连接
            socket = new Socket(host, port);
            // 建立连接后获得输出流
            outputStream = socket.getOutputStream();
            String message = "你好  世界如此美丽";
            outputStream.write(message.getBytes("UTF-8"));
            //通过shutdownOutput告诉服务器已经发送完数据,后续只能接受数据
            socket.shutdownOutput();
            inputStream = socket.getInputStream();
            int len;
            byte[] bytes = new byte[1024];
            StringBuilder sb = new StringBuilder();
            while ((len = inputStream.read(bytes)) != -1) {
                sb.append(new String(bytes, 0, len, "utf-8"));
            }
            System.out.println("从服务端得到的响应是:" + sb);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
                if (null != outputStream) {
                    outputStream.close();
                }
                if (null != socket) {
                    socket.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

客户端也有相应的变化,在发送完消息时,调用关闭输出流方法,然后打开输出流,等候服务端的消息。

最终服务端打印结果如下:

server将一直等待连接的到来
get message from client: 你好  世界如此美丽

客户端打印结果如下:

从服务端得到的响应是:我已经收到客户端发送的数据,这是我的响应结果

这个模式的使用场景一般用在,客户端发送命令给服务器,然后服务器相应指定的命令,如果只是客户端发送消息给服务器,然后让服务器返回收到消息的消息,这就有点过分了,这就是完全不相信Socket的传输安全性,要知道它的底层可是TCP,如果没有发送到服务器端是会抛异常的,这点完全不用担心。

3、如何告知对方已发送完命令

其实这个问题还是比较重要的,正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。所以怎么告知服务端已经发送完消息就显得特别重要。

1> 通过Socket关闭

这个是第一章介绍的方式,当Socket关闭的时候,服务端就会收到相应的关闭信号,那么服务端也就知道流已经关闭了,这个时候读取操作完成,就可以继续后续工作。

但是这种方式有一些缺点

客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息
如果客户端想再次发送消息,需要重新创建Socket连接

2> 通过Socket关闭输出流的方式

这种方式调用的方法是:

socket.shutdownOutput();

而不是(outputStream为发送消息到服务端打开的输出流):

outputStream.close();

如果关闭了输出流,那么相应的Socket也将关闭,和直接关闭Socket一个性质。

调用Socket的shutdownOutput()方法,底层会告知服务端我这边已经写完了,那么服务端收到消息后,就能知道已经读取完消息,如果服务端有要返回给客户的消息那么就可以通过服务端的输出流发送给客户端,如果没有,直接关闭Socket。

调用socket.close() 或者socket.shutdownOutput()方法都可以告诉服务端数据已经发送完毕。

但是这两个方法有本质的区别。

socket.close() 将socket关闭连接,那边如果有服务端给客户端反馈信息,此时客户端是收不到的。

socket.shutdownOutput()是将输出流关闭,此时,如果服务端有信息返回,则客户端是可以正常接受的。

这种方式通过关闭客户端的输出流,告知服务端已经写完了,虽然可以读到服务端发送的消息,但是还是有一点点缺点:

​ 不能再次发送消息给服务端,如果再次发送,需要重新建立Socket连接。

这个缺点,在访问频率比较高的情况下将是一个需要优化的地方。

3> 通过约定符号

这种方式的用法,就是双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法。

假如约定单独的一行为end,代表发送完成,例如下面的消息,end则代表消息发送完成:

Hello World
end
那么服务端响应的读取操作需要进行如下改造:

Socket socket = server.accept();
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
String line;
StringBuilder sb = new StringBuilder();
while ((line = read.readLine()) != null && "end".equals(line)) {
  //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
  sb.append(line);
}

可以看见,服务端不仅判断是否读到了流的末尾,还判断了是否读到了约定的末尾。

这么做的优缺点如下:

优点:不需要关闭流,当发送完一条命令(消息)后可以再次发送新的命令(消息)

缺点:需要额外的约定结束标志,太简单的容易出现在要发送的消息中,误被结束,太复杂的不好处理,还占带宽。

经过了这么多的优化还是有缺点,难道就没有完美的解决方案吗,答案是有的,看接下来的内容。

4> 通过指定长度

如果你了解一点class文件的结构(后续会写,敬请期待),那么你就会佩服这么设计方式,也就是说我们可以在此找灵感,就是我们可以先指定后续命令的长度,然后读取指定长度的内容做为客户端发送的消息。

现在首要的问题就是用几个字节指定长度呢,我们可以算一算:

1个字节:最大256,表示256B
2个字节:最大65536,表示64K
3个字节:最大16777216,表示16M
4个字节:最大4294967296,表示4G
依次类推
这个时候是不是很纠结,最大的当然是最保险的,但是真的有必要选择最大的吗,其实如果你稍微了解一点UTF-8的编码方式(字符编码后续会写,敬请期待),那么你就应该能想到为什么一定要固定表示长度字节的长度呢,我们可以使用变长方式来表示长度的表示,比如:

第一个字节首位为0:即0XXXXXXX,表示长度就一个字节,最大128,表示128B
第一个字节首位为110,那么附带后面一个字节表示长度:即110XXXXX 10XXXXXX,最大2048,表示2K
第一个字节首位为1110,那么附带后面二个字节表示长度:即110XXXXX 10XXXXXX 10XXXXXX,最大131072,表示128K
依次类推
上面提到的这种用法适合高富帅的程序员使用,一般呢,如果用作命名发送,两个字节就够了,如果还不放心4个字节基本就能满足你的所有要求,下面的例子我们将采用2个字节表示长度,目的只是给你一种思路,让你知道有这种方式来获取消息的结尾。

服务端

package org.zpli.socket.server;

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

/**
 * @Description: TODO
 * @author: zpli
 * @Date: 2020/4/16 10:12
 */
public class ServerSocketDemo3 {

    public static void main(String[] args) {
        // 监听指定的端口
        int port = 55533;
        ServerSocket server = null;
        Socket socket = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");
            socket = server.accept();
            // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
            inputStream = socket.getInputStream();
            byte[] bytes;
            // 因为可以复用Socket且能判断长度,所以可以一个Socket用到底
            while (true) {
                // 首先读取两个字节表示的长度
                int first = inputStream.read();
                //如果读取的值为-1 说明到了流的末尾,Socket已经被关闭了,此时将不能再去读取
                if (first == -1) {
                    break;
                }
                int second = inputStream.read();
                int length = (first << 8) + second;
                // 然后构造一个指定长的byte数组
                bytes = new byte[length];
                // 然后读取指定长度的消息即可
                inputStream.read(bytes);
                System.out.println("get message from client: " + new String(bytes, "UTF-8"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
                if (null != outputStream) {
                    outputStream.close();
                }
                if (null != socket) {
                    socket.close();
                }
                if (null != server) {
                    server.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

客户端

package org.zpli.socket.client;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @Description: TODO
 * @author: zpli
 * @Date: 2020/4/16 10:15
 */
public class SocketClient3 {

    public static void main(String args[]) {
        // 要连接的服务端IP地址和端口
        String host = "127.0.0.1";
        int port = 55533;
        Socket socket = null;
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            String message = "你好  我是发送的数据";
            //首先需要计算得知消息的长度
            byte[] sendBytes = message.getBytes("UTF-8");
            //然后将消息的长度优先发送出去
            outputStream.write(sendBytes.length >>8);
            outputStream.write(sendBytes.length);
            //然后将消息再次发送出去
            outputStream.write(sendBytes);
            outputStream.flush();
            //==========此处重复发送一次,实际项目中为多个命名,此处只为展示用法
            message = "第二条消息";
            sendBytes = message.getBytes("UTF-8");
            outputStream.write(sendBytes.length >>8);
            outputStream.write(sendBytes.length);
            outputStream.write(sendBytes);
            outputStream.flush();
            //==========此处重复发送一次,实际项目中为多个命名,此处只为展示用法
            message = "the third message!";
            sendBytes = message.getBytes("UTF-8");
            outputStream.write(sendBytes.length >>8);
            outputStream.write(sendBytes.length);
            outputStream.write(sendBytes);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
                if (null != outputStream) {
                    outputStream.close();
                }
                if (null != socket) {
                    socket.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

客户端要多做的是,在发送消息之前先把消息的长度发送过去。

这种事先约定好长度的做法解决了之前提到的种种问题,Redis的Java客户端Jedis就是用这种方式实现的。

当然如果是需要服务器返回结果,那么也依然使用这种方式,服务端也是先发送结果的长度,然后客户端进行读取。当然现在流行的就是,长度+类型+数据模式的传输方式。

四、服务端优化

在上面的例子中,服务端仅仅只是接受了一个Socket请求,并处理了它,然后就结束了,但是在实际开发中,一个Socket服务往往需要服务大量的Socket请求,那么就不能再服务完一个Socket的时候就关闭了,这时候可以采用循环接受请求并处理的逻辑:

package org.zpli.socket.server;

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

/**
 * @Description: 
 * @author: zpli
 * @Date: 2020/4/16 9:59
 */
public class ServerSocketDemo4 {

    public static void main(String[] args) {
        // 监听指定的端口
        int port = 55533;
        ServerSocket server = null;
        Socket socket = null;
        InputStream inputStream = null;
        try {
            server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");
            while (true) {
                socket = server.accept();
                // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
                inputStream = socket.getInputStream();
                byte[] bytes = new byte[1024];
                int len;
                StringBuilder sb = new StringBuilder();
                while ((len = inputStream.read(bytes)) != -1) {
                    //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
                    sb.append(new String(bytes, 0, len, "UTF-8"));
                }
                System.out.println("get message from client: " + sb);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
                if (null != socket) {
                    socket.close();
                }
                if (null != server) {
                    server.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

这种一般也是新手写法,但是能够循环处理多个Socket请求,不过当一个请求的处理比较耗时的时候,后面的请求将被阻塞,所以一般都是用多线程的方式来处理Socket,即每有一个Socket请求的时候,就创建一个线程来处理它。

不过在实际生产中,创建的线程会交给线程池来处理,为了:

线程复用,创建线程耗时,回收线程慢。

防止短时间内高并发,指定线程池大小,超过数量将等待,方式短时间创建大量线程导致资源耗尽,服务挂掉。

package org.zpli.socket.server;

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Description:
 * @author: zpli
 * @Date: 2020/4/16 9:59
 */
public class ServerSocketDemo5 {

    public static void main(String[] args) {
        // 监听指定的端口
        int port = 55533;
        ServerSocket server = null;

        ExecutorService executorService = Executors.newFixedThreadPool(100);
        try {
            server = new ServerSocket(port);

            // server将一直等待连接的到来
            System.out.println("server将一直等待连接的到来");

            while (true) {
                Socket socket = server.accept();
                executorService.submit(() -> {
                    InputStream inputStream = null;
                    try {
                        // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
                        inputStream = socket.getInputStream();
                        byte[] bytes = new byte[1024];
                        int len;
                        StringBuilder sb = new StringBuilder();
                        while ((len = inputStream.read(bytes)) != -1) {
                            //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
                            sb.append(new String(bytes, 0, len, "UTF-8"));
                        }
                        System.out.println("get message from client: " + sb);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            if (null != inputStream) {
                                inputStream.close();
                            }
                            if (null != socket) {
                                socket.close();
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != server) {
                    server.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

使用线程池的方式,算是一种成熟的方式。可以应用在生产中。

五、定长通讯读取消息长度头

一、前言

数据在网络传输时使用的都是字节流,Socket也不例外,所以我们发送数据的时候需要转换为字节发送,读取的时候也是以字节为单位读取。
那么问题就在于socket通讯时,接收方并不知道此次数据有多长,因此无法精确地创建一个缓冲区(字节数组)用来接收,在不定长通讯中,通常使用的方式时每次默认读取8*1024长度的字节,若输入流中仍有数据,则再次读取,一直到输入流没有数据为止。但是如果发送数据过大时,发送方会对数据进行分包发送,这种情况下或导致接收方判断错误,误以为数据传输完成,因而接收不全。
所以,大部分情况下,双方使用socket通讯时都会约定一个定长头放在传输数据的最前端,用以标识数据体的长度,通常定长头有整型int,短整型short,字符串String三种形式。

二、整型与短整型

int型的长度数据会以4个byte存放,所以可以读取前四个字节的数据,然后转换为int类型:

InputStream is = socket.getInputStream();
byte[] datalen = new byte[4];
is.read(datalen);//读取前四个字节数据存放到datalen中
int length = b[3]&0xFF | (b[2]&0xFF) << 8 | (b[1]&0xFF) << 16 | (b[0]&0xFF) << 24;//将字节数组转换为int型
byte[] data = new byte[length];
is.read(data);
String recvMsg = new String(data);//将获得数据转为字符串类型

当然还有一个更简单的方法,使用DataInputStream:

DataInputStream is = new DataInputStream(socket.getInputStream());
int datalen = is.readInt();//读取前四位字节并返回int类型数据
byte[] data = new byte[datalen];
is.readFully(data);
//使用阻塞的方式读取完整的datalen长度的数据
String recvMsg = new String(data);//将获得数据转为字符串类型

使用DataInputStream不但可以简化代码,而且它的readFully方法会以阻塞等待的形式读取完整datalen长度的数据,而上面的read方法有可能出现没有读取完整的情况,因此对于定长通讯推荐使用DataInputStream和readFully方法;
同样,对于短整型short,DataInputStream也带有对应的方法:

int datalen = is.readShort();//读取前两个字节数据并返回short型数据

同样也可以使用read读取前两位再转为int,在此不多赘述。

三、字符串类型

字符串类型的好处是可以自行约定长度,将长度转为字符串,然后通过左补零的形式达到约定长度。假设约定定长头为8位,数据长度为1480,则这笔数据传输的前八位定长头为“00001480”。
处理方式为先读取约定长度字节,然后直接转为String类型,再转为int型:

InputStream is = socket.getInputStream();
byte[] datalen = new byte[8];
//假设约定8位
is.read(datalen);
int length = Integer.parseInt(new String(datalen));//将字节数组转为字符串,再转为int类型
byte[] data = new byte[length];
is.read(data);
String recvMsg = new String(data);//将获得数据转为字符串类型

四、后记

对于socket短链接通讯,建议使用定长头标识通讯内容长度,并且不宜发送过大的报文,以免通讯过程中产生内容丢失,比如网络丢包等情况。然后接收时尽量使用DataInputStream的readFully方法读取确定长度的数据。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值