目录
一、上篇文章的代码
服务器代码
客户端代码
二、改进原因
三、解决方案(初步)
四、解决方案(进阶)
五、解决方案(高阶)
一、上篇文章的代码
服务器代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket listenSocket = null;
//为什么起名为listenSocket呢?
//原因是在操作系统原生的socket API中(操作系统中提供的一组C语言风格的API)其中有一个API叫做listen
//listen的这个API的功能可以让当前的socket变成一个处理连接的socket(把这个普通的socket变成了一个listenSocket)
//但是在java标准库中,listen方法已经被封装到ServerSocket内部了,我们已经感知不到了
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//UDP的服务器进入主循环,就直接尝试receive读取请求了
//但是TCP是有连接的,要先建立好连接
//当服务器运行的时候,当前是否有客户端来建立连接是不确定的
//如果客户端没有建立连接,accept就会阻塞等待
//如果有客户端建立连接了,此时accept就会返回一个Socket对象
Socket clientSocket = listenSocket.accept();
//服务器和客户端之间进一步的交互,就交给clientSocket来完成了
//那么怎么理解listenSocket和clientSocket分别起的作用呢?
//我们还是通过一个例子来理解
//假设我们准备买房子,而在大街上正好看到一个正在介绍楼盘的小哥
//小哥在知道我的用意后便开车把我拉到了楼盘处,并找到一个售楼小姐专门给我介绍房子的情况
//这位小哥把我介绍给售楼小姐后就走了,不管我了
//此后我买房子的所有情况都由这位小姐负责
//此刻这位小哥就好比是listenSocket,而这位售楼小姐就好比clientSocket
//如果客户端和服务器断开连接了,接下来就会销毁clientSocket
//我们的服务器中有一个listenSocket,而clientSocket可以没有,可以有一个,也可以有多个
//一个客户端只对应着一个clientSocket
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) throws IOException {
//处理一个连接,在这个连接中可能会涉及客户端和服务器之间的多次交互
String log = String.format("[%s:%d] 客户端上线!",
clientSocket.getInetAddress().toString(),
clientSocket.getPort());
System.out.println(log);
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while (true) {
//1.读取请求并解析
// 可以直接通过inputStream的read把数据读到一个byte[],然后再转成一个String
// 这样做太麻烦了,这里可以通过Scanner来完成这个工作
Scanner scanner = new Scanner(inputStream);
//那么什么时候循环结束呢?可以这么做
if (!scanner.hasNext()) {
log = String.format("[%s:%d] 客户端下线!",
clientSocket.getInetAddress().toString(),
clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
//Debug
//System.out.println("["+request+"]");
//2.根据请求计算响应
String response = process(request);
//3.把响应写回客户端
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);//注意格式
writer.flush();
log = String.format("[%s:%d] req: %s ; rep: %s",
clientSocket.getInetAddress().toString(),
clientSocket.getPort(),
request,response);
System.out.println(log);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//当前的clientSocket生命周期不是跟随整个程序,而是和连接相关
//因此需要每个连接结束都进行关闭
//如果不关闭,那么随着连接的增多,socket文件可能会出现资源泄露的情况
clientSocket.close();
}
}
//因为当前是实现一个回显服务器
//这就意味客户端发啥,服务器就回应啥
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
private String serverIp;
private int serverPort;
public TcpEchoClient (String serverIp, int serverPort) throws IOException {
this.serverIp = serverIp;
this.serverPort = serverPort;
//让socket创建的同时,就和服务器尝试建立连接
this.socket = new Socket(serverIp,serverPort);
}
public void start() {
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;
}
//Debug
//System.out.println("["+request+"]");
//2.把这个读取的内容构造成请求,发送给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
//println只是把数据写到缓冲区里,至于到没到IO设备是不确定的
//可以加上一个flush强制发送
printWriter.flush();
//3.从服务器读取响应并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4.把结果显示在界面上
String log = String.format("req:%s ; resp:%s",request,response);
System.out.println(log);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
二、改进原因
现在的我们只是一个客户端和服务器通信,在实际的开发中,一个服务器应该要对应很多个客户端,甚至是成千上万个客户端,因此我们需要很多台客户端和服务器进行测试
在测试之前我们需要在一些地方进行修改
第5步中把选中的选项打上对勾即可。
在修改完成之后,我们先开始运行服务器,然后运行客户端,然后多次运行客户端,就会得到多台运行的客户端。
当前我们发现,当启动第二个客户端的时候,服务器并没有提示上线,而且第二个客户端发送数据(hello2)的时候,客户端和服务器这边都没反应
当我们退出第一个客户端的时候,服务器立刻就提示了第二个客户端上线,并且第二个客户端也得到了hello2这样的响应。
通过这些我们得到结论:当前我们的服务器在同一时间只能给一台客户端提供服务,只有前一个客户端下线了,后一个客户端才能上线。
通过研究服务器的代码得知
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//UDP的服务器进入主循环,就直接尝试receive读取请求了
//但是TCP是有连接的,要先建立好连接
//当服务器运行的时候,当前是否有客户端来建立连接是不确定的
//如果客户端没有建立连接,accept就会阻塞等待
//如果有客户端建立连接了,此时accept就会返回一个Socket对象
Socket clientSocket = listenSocket.accept();
//服务器和客户端之间进一步的交互,就交给clientSocket来完成了
//那么怎么理解listenSocket和clientSocket分别起的作用呢?
//我们还是通过一个例子来理解
//假设我们准备买房子,而在大街上正好看到一个正在介绍楼盘的小哥
//小哥在知道我的用意后便开车把我拉到了楼盘处,并找到一个售楼小姐专门给我介绍房子的情况
//这位小哥把我介绍给售楼小姐后就走了,不管我了
//此后我买房子的所有情况都由这位小姐负责
//此刻这位小哥就好比是listenSocket,而这位售楼小姐就好比clientSocket
//如果客户端和服务器断开连接了,接下来就会销毁clientSocket
//我们的服务器中有一个listenSocket,而clientSocket可以没有,可以有一个,也可以有多个
//一个客户端只对应着一个clientSocket
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) throws IOException {
//处理一个连接,在这个连接中可能会涉及客户端和服务器之间的多次交互
String log = String.format("[%s:%d] 客户端上线!",
clientSocket.getInetAddress().toString(),
clientSocket.getPort());
System.out.println(log);
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while (true) {
//1.读取请求并解析
// 可以直接通过inputStream的read把数据读到一个byte[],然后再转成一个String
// 这样做太麻烦了,这里可以通过Scanner来完成这个工作
Scanner scanner = new Scanner(inputStream);
//那么什么时候循环结束呢?可以这么做
if (!scanner.hasNext()) {
log = String.format("[%s:%d] 客户端下线!",
clientSocket.getInetAddress().toString(),
clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
//Debug
//System.out.println("["+request+"]");
//2.根据请求计算响应
String response = process(request);
//3.把响应写回客户端
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);//注意格式
writer.flush();
log = String.format("[%s:%d] req: %s ; rep: %s",
clientSocket.getInetAddress().toString(),
clientSocket.getPort(),
request,response);
System.out.println(log);
}
}
如果第一个客户端没有退出,此时服务器的逻辑一直在processConnection()内部打转,是没有机会再次调用accept的,从而也就没有机会处理第二个客户端的请求了。
三、解决方案(初步)
解决这个问题的方法就是使用多线程
主线程里面循环调用accept,每次获取到一个连接,就能创建一个线程,让这个线程处理这个连接。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpThreadEchoServer {
private ServerSocket listenSocket= null;
public TcpThreadEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//在这个代码中,通过创建线程,就能保证accept调用完毕之后,能够立刻再次调用accept
Socket clientSocket = listenSocket.accept();
//创建一个线程来给这个客户提供服务
Thread t = new Thread() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
t.start();
}
}
public void processConnection(Socket clientSocket) throws IOException {
String log = String.format("[%s:%d] 客户端上线!",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
System.out.println(log);
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while (true) {
//1.读取请求并解析
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
log = String.format("[%s:%d] 客户端下线!",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回客户端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
log = String.format("[%s:%d] req:%s ; res: %s",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
System.out.println(log);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
//当前是回显服务器
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer tcpThreadEchoServer = new TcpThreadEchoServer(9090);
tcpThreadEchoServer.start();
}
}
变动就是这里的代码
while (true) {
//在这个代码中,通过创建线程,就能保证accept调用完毕之后,能够立刻再次调用accept
Socket clientSocket = listenSocket.accept();
//创建一个线程来给这个客户提供服务
Thread t = new Thread() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
t.start();
}
客户端的代码和之前的一样
运行结果为
在实际的开发中,客户端的数量会非常的多,此时问题又来了
虽然线程比进程更轻量,但是,如果有很多的客户端连接又退出,这就会导致当前的服务器会频繁的创建销毁线程,这个时候还是会有很大的成本。
四、解决方案(进阶)
那么该如何解决这个问题呢?
答案就是线程池
代码其它地方改动不大,就这儿需要重点掌握
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
Socket clientSocket = listenSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
那么如果是有数万个进程同时向服务器发送请求数据的话那该怎么办呢?
五、解决方案(高阶)
解决的方法有三个:
1.协程
2.IO多路复用技术
3.使用多个主机(分布式),(提供更多的硬件资源)
这三点都是基本的来处理一个高并发场景所使用的办法
|