一、前言
前面一章节已经知道了关于Socket的一些理论知识,下面来看一下现实中是怎样开发的。
二、实例
先来一个简单的demo
//服务端
public class SocketServer {
@SuppressWarnings("CharsetObjectCanBeUsed")
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(9999);
ExecutorService executorService = Executors.newFixedThreadPool(100);
while (true) {
Socket socket = serverSocket.accept();
Runnable runnable = () -> {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8"));
String string;
while ((string = bufferedReader.readLine()) != null) {
System.out.println("客户端说:" + string);
}
} catch (IOException e) {
e.printStackTrace();
}
};
executorService.submit(runnable);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//客户端1
public class SocketClient1 {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
String string = bufferedReader.readLine();
bufferedWriter.write(string);
bufferedWriter.write("\n");
bufferedWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//客户端2
public class SocketClient2 {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
while (true) {
String string = bufferedReader.readLine();
bufferedWriter.write(string);
bufferedWriter.write("\n");
bufferedWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面使用的线程池优势:
但是还有问题就是上面的服务端判断结束是通过换行符(读取到的一行为null)来判定的,这样的优缺点是:
那么有没有一种好的解决方案呢,其实是有的,就是定长字符串,下面来介绍:
如果你了解一点class文件的结构,就知道这种思想了,就是我们可以先指定后续命令的长度,然后读取指定长度的内容做为客户端发送的消息。
现在首要的问题就是用几个字节指定长度呢,我们可以算一算:
这个时候是不是很纠结,最大的当然是最保险的,但是真的有必要选择最大的吗,其实如果你稍微了解一点UTF-8的编码方式,那么你就应该能想到为什么一定要固定表示长度字节的长度呢,我们可以使用变长方式来表示长度的表示,比如:
上面提到的这种用法适合高富帅的程序员使用,一般呢,如果用作命名发送,两个字节就够了,如果还不放心4个字节基本就能满足你的所有要求,下面的例子我们将采用2个字节表示长度,目的只是给你一种思路,让你知道有这种方式来获取消息的结尾:
//服务端
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream inputStream = null;
try {
serverSocket = new ServerSocket(9999);
System.out.println("服务端将一直等待客户端的到来。。。。。。。");
while (true) {
socket = serverSocket.accept();
inputStream = socket.getInputStream();
byte[] bytes;
//读取第一个字节,如果是-1说明已经结束了
int first = inputStream.read();
if (first == -1) {
break;
}
//读取第二个字节
int second = inputStream.read();
//按照双方约定的长度算法进行计算
int length = (first << 8) + second;
bytes = new byte[length];
inputStream.read(bytes);
System.out.println("接受的数据为:" + new String(bytes, "utf-8"));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
serverSocket.close();
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
inputStream.close();
}
}
}
}
//客户端
public class SocketClient1 {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1",9999);
OutputStream outputStream = socket.getOutputStream();
String msg = "你好,我是joy";
byte[] len = msg.getBytes("utf-8");
outputStream.write(len.length>>8);
outputStream.write(len.length);
outputStream.write(len);
outputStream.flush();
msg = "this is second";
len = msg.getBytes("utf-8");
outputStream.write(len.length>>8);
outputStream.write(len.length);
outputStream.write(len);
outputStream.flush();
msg="this didnd g erfgaorg sadglkwene fs是否v哦i是";
len = msg.getBytes("utf-8");
outputStream.write(len.length>>8);
outputStream.write(len.length);
outputStream.write(len);
outputStream.flush();
outputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如果需要服务端返回结果也是采用这种方式,然后客户端进行读取,现在比较流行的就是长度+类型+数据模式的传输方式。
三、问题
那么是不是按照上面的方式来说就不会出现问题呢,那肯定不是,我们需要考虑拆包和黏包的问题,下面是产生原因
首先可以明确的是,大部分情况下我们是不希望发生拆包和黏包的(如果希望发生,什么都去做即可),那么怎么去避免呢,下面进行详解?
首先我们应该正确看待黏包,黏包实际上是对网络通信的一种优化,假如说上层只发送一个字节数据,而底层却发送了41个字节,其中20字节的I P首部、 20字节的T C P首部和1个字节的数据,而且发送完后还需要确认,这么做浪费了带宽,量大时还会造成网络拥堵。当然它还是有一定的缺点的,就是因为它会合并一些包会导致数据不能立即发送出去,会造成延迟,如果能接受(一般延迟为200ms),那么还是不建议关闭这种优化,如果因为黏包会造成业务上的错误,那么请改正你的服务端读取算法(协议),因为即便不发生黏包,在服务端缓存区也可能会合并起来一起提交给上层,推荐使用长度+类型+数据模式。
如果不希望发生黏包,那么通过禁用TCP_NODELAY即可,Socket中也有相应的方法:
void setTcpNoDelay(boolean on)
通过设置为true即可防止在发送的时候黏包,但是当发送的速率大于读取的速率时,在服务端也会发生黏包,即因服务端读取过慢,导致它一次可能读取多个包。
这个问题应该引起重视,在TCP/IP详解中说过:最大报文段长度(MSS)表示TCP传往另一端的最大块数据的长度。当一个连接建立时,连接的双方都要通告各自的 MSS。客户端会尽量满足服务端的要求且不能大于服务端的MSS值,当没有协商时,会使用值536字节。虽然看起来MSS值越大越好,但是考虑到一些其他情况,这个值还是不太好确定,具体详见《TCP/IP详解 卷1:协议》。
如何应对拆包,其实在上面2.3节已经介绍过了,那就是如何表明发送完一条消息了,对于已知数据长度的模式,可以构造相同大小的数组,循环读取,示例代码如下:
int length = 1024;//这个是读取的到数据长度,现假定1024
byte[] data = new byte[1024];
int readLength = 0;
while(readLength
这样当循环结束后,就能读取到完整的一条数据,而不需要考虑拆包了。
四、其他参数
其实如果经常看有关网络编程的源码的话,就会发现Socket还是有很多设置的,可以学着用,但是还是要有一些基本的了解比较好。下面就对Socket的Java API中涉及到的进行简单讲解。首先呢Socket有哪些可以设置的选项,其实在SocketOptions接口中已经都列出来了:
参考:
https://www.cnblogs.com/yiwangzhibujian/p/7107785.html