一.TCP流套字节相关API.
Socket(既能给客户端使用,也能给服务器使用)
构造方法
基本方法:
ServerSocket(只能给服务器使用)
构造方法:
基本方法:
二.TCP实现回显服务器.
客户端代码示例:
bash
package Demo2;
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 clientSocket =null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//此处可以把这里的IP和port直接传给socket对象.
//由于TCP是有连接的,所以socket中就会保存好这两个信息.
clientSocket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动~~");
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()
) {
Scanner scannerConsole = new Scanner(System.in);
//从控制台读取数据
Scanner scannerNetWork = new Scanner(inputStream);
//
while(true){
//1.从控制台读取数据.
System.out.println("->");
if(!scannerConsole.hasNext()){
break;
}
String request = scannerConsole.next();
PrintWriter printWriter = new PrintWriter(outputStream);
//2.把请求发送给服务器. 这里要使用println来发送.为了让发送的请求末尾带有一个换行.
printWriter.println(request);
//通过flush来主动刷新缓冲区,来确保数据发送到服务器了.
printWriter.flush();
//3.从服务器读取响应.这里也是和服务器返回响应的逻辑想对应
String response = scannerNetWork.next();
//4.把响应打印到控制台.
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
服务器代码示例:
bash
package Demo2;
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动~~");
ExecutorService pool = Executors.newCachedThreadPool();
while(true) {
//通过accept方法来接听电话,然后才能进行通信.
Socket clientSocket = serverSocket.accept();
// Thread thread = new Thread(()->{
// processConnection(clientSocket);
// });
// thread.start();
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
//通过这个方法来处理一次连接,连接过程中就会涉及请求响应交互
public void processConnection(Socket clientSocket){
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//循环读取客户端的请求并返回响应
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()
) {
Scanner scanner = new Scanner(inputStream);
while(true){
//可以通过inputStream来读取数据了.
//byte[] buffer = new byte[4096];
//int n = inputStream.read(buffer);
//此处读操作完全可以用read来完成,但是read是把读取到的数据放到一个byte数组之中
//后续根据请求处理响应,还需要把数组转化成字符串.
//此时就可以使用Scanner来简化这个过程.
if(!scanner.hasNext()){
//读取完毕,例如客户端断开链接.
System.out.printf("[%s %d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
//1.读取请求并解析,此时有一个隐藏的约定,next读的时候要读到空白符才会结束
// 因此就要求客户端发来的请求必须带有空白符结尾.比如带有/n或" ".
String request = scanner.next();
//2.根据请求计算响应.
String response = process(request);
//3.把相应给客户端.
//outputStream.write(response.getBytes(),0,response.getBytes().length);
// 通过这种方式可以返回,但是这种方式不方便给返回的响应中添加换行
// 此时就可以给outputStream套一层来完成更方便的写入.
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s %d] request : %s ;response : %s ",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
System.out.println();
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
运行结果:
代码执行流程:
- 服务器启动,阻塞在accept,等待客户端建立连接.
- 客户端启动.这里的new操作会触发和服务器之间建立连接的操作.此时服务器就会从accept中返回.
- 服务器解除阻塞,继续向下执行,执行processConnection方法
执行这个方法,执行到hasNext就会阻塞,此时虽然建立了连接,但是客户端还没有发来任何请求.hasNext阻塞等待到请求到达. - 客户端继续执行到hasNext,等待用户向客户端写入内容.
- 如果用户真的输入了,就会继续向下执行发送请求等待返回的逻辑.
这里就会把请求真的发出去,同时客户端等待服务器返回响应,此时next就会阻塞等待. - 服务器从hasNext 返回读取到的请求,构造响应,并把响应返回给客户端.
此时服务器结束此次循环,开启下一次循环,继续阻塞在hasNext等待下一个请求 - 客户端读取到响应,并显示出来.
此时客户端就会结束此次循环,开启下一次循环,继续阻塞在hasNext等待用户输入下一个请求.
代码注意事项:
-
- flush()方法存在一个内存缓冲区.由于文件IO的操作比较低效,因此就希望IO的次数少一些,等攒到一定程度再进行IO操作.(相当于多次IO合并成一次了). 因此就引入了缓冲区,此时就会出现问题,你输入的数据比较少,数据被存在内存缓冲区了,所以需要我们手动刷新缓冲区.
-
- 如果客户端非常的多,就需要创建多个Socket对象,此时就可能导致系统的资源使用完了,因此需要在Socket执行完毕之后关闭资源.
-
- 引入线程池来解决频繁的创建销毁线程.
-
- 如果有多个客户端建立请求,并且长时间不销毁
- 解决方案一:引入协程===>轻量级线程,用户态可以通过手动调度的方式让一个线程并发的做多个任务.
- 解决方案二:IO多路复用===>这是一个系统内核级别的机制,本质上是让一个线程去处理多个Socket对象 (这些Socket数据并非是同一时刻都需要处理).