1、基础用法,双向通信,发送消息并接受消息
Socket的底层是TCP,不需要考虑服务端是否已经接收到消息,如果没有发送到服务器端是会抛异常的。
Java的socket是一个全双工套接字,任何的输入流或输出流的close()都会造成Socket关闭。
解决办法:使用socket.shutdownOutput()方法关闭套接字的输出流,使服务器知道输出流关闭,可以得到流末尾标志(-1)。
同样,可以使用socket.shutdownInput()方法单独关闭套接字的输入流。
在客户端或者服务端通过socket.shutdownOutput()都是单向关闭的,即关闭客户端的输出流并不会关闭服务端的输出流,所以是一种单方向的关闭流;
通过socket.shutdownOutput()关闭输出流,但socket仍然是连接状态,连接并未关闭
如果直接关闭输入或者输出流,即:in.close()或者out.close(),会直接关闭socket
服务端
package com.study;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Socket服务端
*/
public class SocketServer {
public static void main(String[] args) throws Exception {
// 监听指定的端口
int port = 55533; //一般使用49152到65535之间的端口
ServerSocket server = new ServerSocket(port);
// 使用线程池,防止过多线程耗尽资源
ExecutorService threadPool = Executors.newFixedThreadPool(100);
Socket socket;
while (true) {
socket = server.accept(); //会一直阻塞,直到有客户端连接进来
// new Thread 只是创建一个类的对象实例而已。而真正创建线程的是start()方法。
// 这里并没有直接调用start()方法,所以并没创建新线程,而是交给线程池去执行。
threadPool.submit(new SocketThread(socket));
}
// socket.close();
// server.close();
}
static class SocketThread implements Runnable {
private Socket socket;
public SocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
// 建立好连接后,从socket中获取输入流
inputStream = socket.getInputStream();
// 建立好连接后,从socket中获取输出流
outputStream = socket.getOutputStream();
byte[] buf = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
//只有当客户端关闭它的输出流的时候,服务端才能取得结尾的-1
while ((len = inputStream.read(buf)) != -1) {
// 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(buf, 0, len, StandardCharsets.UTF_8));
}
System.out.println("收到客户端消息:" + sb);
outputStream.write("Hello Client".getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端
package com.study;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* Socket客户端
*/
public class SocketClient {
public static void main(String[] args) throws Exception {
Socket socket = null;
InputStream inputStream = null;
OutputStream outputStream = null;
try {
// 要连接的服务端IP地址和端口
String host = "127.0.0.1";
int port = 55533;
// 与服务端建立连接
socket = new Socket(host, port);
// 建立好连接后,从socket中获取输入流
inputStream = socket.getInputStream();
// 建立好连接后,从socket中获取输出流
outputStream = socket.getOutputStream();
outputStream.write("Hello Server".getBytes(StandardCharsets.UTF_8));
// outputStream.close(); //虽然close()方法也可以发送-1终止符号,但是close()方法会导致socket关闭。
socket.shutdownOutput();// 单向关闭输出流,发送流的终止符-1。
byte[] buf = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
//只有当客户端关闭它的输出流的时候,服务端才能取得结尾的-1
while ((len = inputStream.read(buf)) != -1) {
// 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(buf, 0, len, StandardCharsets.UTF_8));
}
System.out.println("收到服务端消息:" + sb);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2、如何告知对方已发送完命令
其实这个问题还是比较重要的,正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。所以怎么告知服务端已经发送完消息就显得特别重要。
2.1 通过Socket关闭
当Socket关闭的时候,服务端就会收到响应的关闭信号,那么服务端也就知道流已经关闭了,这个时候读取操作完成,就可以继续后续工作。
但是这种方式有一些缺点,客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息
如果客户端想再次发送消息,需要重现创建Socket连接
2.2 通过Socket关闭输出流的方式
这种方式调用的方法是:socket.shutdownOutput();
而不是outputStream.close();如果关闭了输出流,那么相应的Socket也将关闭,和直接关闭Socket一个性质。
调用Socket的shutdownOutput()方法,底层会告知服务端我这边已经写完了,那么服务端收到消息后,就能知道已经读取完消息,如果服务端有要返回给客户的消息那么就可以通过服务端的输出流发送给客户端,如果没有,直接关闭Socket。
这种方式通过关闭客户端的输出流,告知服务端已经写完了,虽然可以读到服务端发送的消息,但是还是有一点点缺点:
不能再次发送消息给服务端,如果再次发送,需要重新建立Socket连接
这个缺点,在访问频率比较高的情况下将是一个需要优化的地方。
2.3 通过约定符号
这种方式的用法,就是双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法。
假如约定单端的一行为end,代表发送完成,例如下面的消息,end则代表消息发送完成:
那么服务端响应的读取操作需要进行如下改造:
while ((line = read.readLine()) != null && "end".equals(line)) {
//注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(line);
}
这么做的优缺点如下:
优点:不需要关闭流,当发送完一条命令(消息)后可以再次发送新的命令(消息)
缺点:需要额外的约定结束标志,太简单的容易出现在要发送的消息中,误被结束,太复杂的不好处理,还占带宽
2.4 通过指定长度
可以先指定后续命令的长度,然后读取指定长度的内容做为客户端发送的消息。