前言
在我的另一篇文章中,有简单介绍过Socket的相关概念链接:SpringBoot简单集成WebSocket
初步了解后,本次再进行一个深入通俗的理解。
Socket作为一种通信机制,通常也被称为"套接字"。
它类似于人们之间的"打电话行为"。我们将每个人的电话号作为独立端口。两个人打电话之前则首先需要其中一方知晓另一方的"端口"。然后申请向对方进行拨号呼叫(请求连接)。
此时被连接方如果正好空闲,接起电话,则双方正式达成连接。开始正式通讯.只要有其中一方挂断电话,则关闭Socket连接。
Socket的两种类型
1.流式Socket(Stream).一种面向连接的Socket,针对面向连接的TCP服务应用,安全但效率低。属于业内较为常用的一种方式
2.数据报式Socket(DataGram).一种无连接的Socket.不安全,效率高.
正文
下面开始在SpringBoot中实战整合Socket
在JDK1.8中.官方整合了Socket服务至java.net包中。因此不需要引入其它依赖。
1.先在配置文件中配置Socket监听的端口
#Socket配置
socket:
port: 8082
2.配置Socket连接类
@Slf4j
@Component
public class SocketServerConfig {
//注入被开放的端口
@Value("${socket.port}")
private Integer port;
//Socket服务是否启动的标识
private boolean socketStart = false;
//Socket服务
public static ServerSocket serverSocket = null;
//当前连接用户数
public static Integer userCount = 1;
//客户端缓存信息
public static ConcurrentHashMap<String, ClientSocket> clientsMap = new ConcurrentHashMap<>();
}
3.配置自定义客户端类
@Data
@Slf4j
public class ClientSocket implements Runnable {
private Socket socket;
private ObjectInputStream inputStream;
private ObjectOutputStream outputStream;
private String key;
private String message;
private final IDataSocket dataSocket= ApplicationContextProvider.getBean((IDataSocket.class));
@Override
public void run() {
// 另起一个线程在指定时间内连接一次客户端
while (true) {
try {
//设定为10s/次
TimeUnit.SECONDS.sleep(5);
if (isSocketClosed(this)) {
logout();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 注册socket到map里
*/
public static ClientSocket register(Socket socket, int count) {
ClientSocket client = new ClientSocket();
try {
log.info("客户端IP:{},用户:{},正在连接Socket服务...", socket.getInetAddress().getHostAddress(), count);
client.setSocket(socket);
client.setInputStream(new ObjectInputStream(socket.getInputStream()));
client.setOutputStream(new ObjectOutputStream(socket.getOutputStream()));
client.setKey("user" + count);
DataApplySocketServer.clientsMap.put(client.getKey(), client);
DataApplySocketServer.userCount++;
log.info("客户端IP:{},用户:{},连接Socket服务成功...", socket.getInetAddress().getHostAddress(), count);
return client;
} catch (Exception e) {
client.logout();
return null;
}
}
/**
* 登出操作, 关闭各种流
*/
public void logout() {
SocketServerConfig.clientsMap.remove("user" + key);
try {
log.info("用户:{}执行登出操作", key);
inputStream.close();
outputStream.close();
SocketServerConfig.userCount--;
log.info("用户:{}执行登出完成", key);
} catch (Exception e) {
log.error("关闭Socket输入/输出异常", e);
} finally {
try {
socket.close();
} catch (Exception e) {
log.error("关闭socket异常", e);
}
}
}
/**
* 发送数据包,判断数据连接状态
*/
public boolean isSocketClosed(ClientSocket clientSocket) {
try {
clientSocket.getSocket().sendUrgentData(1);
//执行业务处理
dataSocket.executeBusinessCode(this);
return false;
} catch (IOException e) {
return true;
}
}
}
4.在Socket连接类中配置Start启动方法
public void start() {
try {
//创建Socket服务
serverSocket = new ServerSocket(port);
log.info("socket在端口:{}中开启", port);
socketStart = true;
} catch (Exception e) {
e.printStackTrace();
System.exit(0);
}
try {
while (socketStart) {
//接受本次连接
Socket socket = serverSocket.accept();
//保持监听
socket.setKeepAlive(true);
//调用自定义客户端配置类中的注册方法,将本次连接的用户注册进去
ClientSocket clientSocket = ClientSocket.register(socket, userCount);
if (clientSocket != null) {
//注册成功后,使用ExecutorService的submit方法,让自定义客户端配置类
//的run方法进行执行
executorService.submit(clientSocket);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.为了避免打成war包后无法启动Socket服务,需要在Application启动类的main方法中添加如下代码
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application = SpringApplication.run(SocketApplication.class, args);
//避免打包为war后,无法启动Socket服务
//在spring容器启动后,取到已经初始化的SocketServer,启动Socket服务,start中可填写端口号,若不填写,默认按照配置文件中的端口号
application.getBean(SocketServerConfig.class).start();
}
6.定义业务接口
public interface IDataSocket{
/**
* 从Socket中接受到的代码,并执行
* @param socket socket
*/
void executeBusinessCode(ClientSocket socket);
}
7.定义业务实现类
@Slf4j
@Service
public class DataSocketImpl implements IDataSocket {
@Override
@SneakyThrows
public void executeBusinessCode(ClientSocket socket) {
ObjectInputStream ois = socket.getInputStream();
ObjectOutputStream oos = socket.getOutputStream();
//这里的Object可以是Json对象或普通的String字符
Object object = ois.readObject();
if (ObjectUtil.isNotEmpty(object)) {
String logId = UUID.randomUUID().toString().toUpperCase();
log.info("监听到客户端信息,监听日志ID为:{}", logId);
String responseMsg = "";
try {
//拿到数据后执行的业务代码
responseMsg = "success"
} catch (DataInterfaceException exception) {
exception.printStackTrace();
//失败后的消息
responseMsg = "error"
}
// 输出流发送返回参数
log.info("客户端信息消息处理完毕,日志ID为:{}", logId);
oos.writeUTF(responseMsg);
oos.flush();
}
}
}
8.简单测试(可另起一个SpringBoot服务)
public static void main(String[] args) throws Exception {
Socket socket = null;
ObjectInputStream ois = null;
ObjectOutputStream oos = null;
try {
//建立连接
socket = new Socket("127.0.0.1", 8082);
// 输出流写数据
oos = new ObjectOutputStream(socket.getOutputStream());
// 输入流读数据
ois = new ObjectInputStream(socket.getInputStream());
test(oos, ois);
} catch (IOException e) {
e.printStackTrace();
} finally {
ois.close();
oos.close();
socket.close();
}
}
@SneakyThrows
public static void test(ObjectOutputStream oos, ObjectInputStream ois) {
for (int i = 0; i < 5; i++) {
log.info("测试第" + i + "次发送数据");
// 输出流给服务端发送数据
oos.writeObject("测试第" + i + "次发送数据");
oos.flush();
// 输入流接收服务端返回的数据
String message = ois.readUTF();
System.out.println(message);
//休息三秒后进入下一次循环
TimeUnit.SECONDS.sleep(3);
}
}