专栏简介: JavaEE从入门到进阶
题目来源: leetcode,牛客,剑指offer.
创作目标: 记录学习JavaEE学习历程
希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
1.Java 流套接字编程模型
2.SeverSocket API
SeverSocket 是创建 TCP 服务端 Socket 的 API.
SeverSocket 的构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字 Socket,并绑定到指定端口 |
ServerSocket 方法:
方法签名 | 方法说明 |
Socket accept() | 开始监听端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接 , 否则阻塞等待. |
void close() | 关闭此套接字 |
3.Socket API
Socket 是客户端 Socket , 或服务器接收到客户端建立连接(accept方法) 的请求后 , 返回的服务端 Socket.
不管是客户端还是服务端 Socket , 都是双方建立连接后 , 保存的对端信息 , 及用来与对方收发数据的.
Socket 构造方法:
方法签名 | 方法说明 |
Socket(String host,int port) | 创建一个客户端流套接字Socket,并与对应的 IP 主机上,对应的端口号建立连接. |
Socket 方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回套接字的输入流 |
InputStream getOutputStream() | 返回此套接字的输入流 |
4.TCP 中的长短连接
TCP 发送数据时 , 需要建立连接 , 什么时候关闭连接就决定是短连接还是长连接.
- 短连接: 每次接收数据并返回响应后 , 都关闭连接(短连接只能收发一次数据)
- 长连接: 不关闭连接 , 一直保持连接状态 , 双方不停的收发数据(长连接可以多次的收发数据)
对比以上长短连接 , 两者区别如下:
- 建立连接 , 关闭连接的耗时: 短连接每次请求 , 响应都需要建立连接 , 关闭连接; 而长连接只需要第一次建立连接 , 之后的请求 , 响应都可以直接传输. 相对来说连接的建立和关闭都是需要耗时的 , 因此长连接更加高效.
- 主动发送请求不同: 短连接一般是客户端主动向服务器发送请求; 而长连接客户端主动发送请求 , 也可以是服务端主动发送.
- 两者的使用场景不同: 短连接适用于客户端请求频率不高的场景 , 如浏览网页等. 长连接适用于客户端与服务端通信频繁的场景 , 如聊天室 , 实时游戏等.
扩展了解:
基于BIO(同步阻塞IO) 的长连接会一直占用系统资源. 对于并发要求很高的服务系统来说 , 这样的消耗是不能承受的.
由于每个连接都需要不停的阻塞等待接收数据 , 所以每个连接都会在一个线程中运行.
一次阻塞对应着一次请求 , 响应 , 不停处理也就是长连接的特性: 一直不关闭连接 , 不停的处理请求.
实际应用时: 服务端一般是基于 NIO (即同步非阻塞IO) 来实现长连接 , 性能可以极大的提升.(IO多路复用)
5. 代码示例:
服务器:
public class TcpEchoSever {
private ServerSocket serverSocket = null;
public TcpEchoSever(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器!");
while (true) {
//使用这个 clientSocket 和具体的客户端进行交流.
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
//使用这个方法来处理一个连接
//这一个连接对应到一个客户端 , 但这里可能会涉及到多次交互
private void processConnection(Socket clientSocket) {
System.out.printf("[%s %d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 基于上述 socket 对象和客户端机通信
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//由于要处理多个请求响应 , 也需要使用循环来进行
while (true) {
//1.读取请求
Scanner scan = new Scanner(inputStream);
if (!scan.hasNext()) {
//没有下个数据 , 说明读完了(客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//此处使用的 next 返回结果中不包含空白符.
String request = scan.next();
//2.根据请求计算响应
String response = process(request);
//3.返回响应
PrintWriter printWriter = new PrintWriter(outputStream);
//此处使用 println 来写入 , 让结果带有一个 \n 来换行 , 方便对端接收解析
printWriter.println(response);
//flush 用来刷新缓冲区 , 保证当前写入的数据确实发出去了.
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp %s \n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoSever server = new TcpEchoSever(9090);
server.start();
}
}
客户端:
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp , int serverPort) throws IOException {
// Socket 构造方法 , 能够识别点分十进制格式的 IP 地址 , 比 DatagramPacket 更方便
// new 这个对象的同时就会和服务器进行 TCP 连接操作.
socket = new Socket(serverIp , serverPort);
}
public void start(){
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true){
//1. 先从键盘上读取到用户输入的内容
System.out.println(">");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("goodbye");
break;
}
//2. 把读到的内容构造成请求发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3. 读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4. 把响应显示到界面上
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1" , 9090);
tcpEchoClient.start();
}
}
由这段代码可知 , 服务器每连接一个客户端 , 就会阻塞等待客户端发送请求 , 如果客户端不主动断开连接 , 该线程就会处于占用状态 , 因此单线程情况下 , 一个服务器只能连接一个客户端.
public void start() throws IOException {
System.out.println("启动服务器!");
while (true) {
//使用这个 clientSocket 和具体的客户端进行交流.
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
为了满足连接多个客户端的需求 , 我们需要创建多个线程 , 但不断地创建/销毁线程 , 需要很大的开销 , 因此可以考虑使用线程池.
public void start() throws IOException {
System.out.println("启动服务器!");
ExecutorService threadPool = Executors.newCachedThreadPool();
while(true){
//使用这个 clientSocket 和具体的客户端进行交流.
Socket clientSocket = serverSocket.accept();
//多线程版本
// Thread t = new Thread(()->{
// processConnection(clientSocket);
// });
// t.start();
//线程池版本
threadPool.submit(()->{
processConnection(clientSocket);
});
}
}
运行结果:
如果开启多个客户端?