回显服务器表示客户端传来的请求是什么,服务器就回应什么,客户端不用对传来的数据进行处理,主要是为了熟悉TCP协议提供的API的使用
对于代码的解释全作为注释写在了代码上,推荐复制到编程软件中查看
UDP协议实现回显服务器可以看UDP数据报网络编程(实现简单的回显服务器,客户端)
其中涉及到的线程池的内容可以看通过标准库创建线程池
idea开启多个客户端的方法可以看idea如何开启多个客户端(一个代码开启多个客户端运行)文章来源:https://uudwc.com/A/ABgRB
服务器代码
package TcpEcho;
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;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wuyulin
* Date: 2023-08-10
* Time: 16:25
*/
//TCP协议 回显服务器(客户端传来的请求是什么,服务器就回应什么)
//TCP协议网络编程两个最主要的类是ServerSocket和Socket
public class TcpEchoServer {
//线程池对象
ExecutorService executorService= null;
//ServerSocket类内置了一个队列,在内核中客户端和服务器尝试建立连接,连接建立完了以后得到的连接对象就存入了队列中
//客户端和服务器尝试建立连接,进行一系列的数据交互称为“握手”,这个过程建立完了以后,连接就建立好了
private ServerSocket serverSocket=null;
//在构造方法中实例化serverSocket对象,port参数表示在创建服务器时要指定服务器的ip地址
TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
executorService=Executors.newCachedThreadPool(); //实例化一个线程数目动态变化的线程池
}
public void start() throws IOException {
//写日志,方便观察当前运行状态
System.out.println("服务器启动");
//服务器需要不停的去接收客户端传来的请求并做出回应,所以需要一个死循环
while (true){
//TCP是有连接的,需要处理服务器与客户端之间的连接,而UDP是无连接的,不需要处理
//处理服务器与客户端之间的连接
//通过调用accept方法,取出serverSocket对象内置的队列中的连接对象(Socket对象)
//当队列中没有Socket连接对象时,就会在accept方法这里进入阻塞等待,直到取得连接对象为止
Socket socket=serverSocket.accept();
//对获得的服务器与客户端之间的连接对象进行处理
//采用当前的编写方式会发现当有多个客户端来连接服务器并发出请求时,服务器不能同时处理
//因为在handleConnection方法中会一直循环处理客户端发出的请求
//而当前编写方式要等handleConnection方法执行完毕才能去取出下一个连接对象,才能处理下一个客户端发出的请求
// handleConnection(socket);
//解决办法:用主线程去调用accept方法取出连接对象,每取出一个连接对象就创建一个线程去处理连接对象对应的客户端发出的请求
//但下面这个创建线程来处理socket连接对象的代码也注释掉了,这是因为要是有很多客户端来对服务器发出请求,就会涉及到大量
//线程的创建和销毁,这是很消耗资源的,所以应该采用线程池来处理socket连接对象
// Thread t=new Thread(()->{
// try {
// handleConnection(socket);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// });
//
// t.start();
//在该服务器类的成员属性中添加上线程池
//将处理连接对象的任务通过submit添加到线程池的阻塞队列中(线程池中的线程会将添加到阻塞队列中的任务进行处理)
executorService.submit(new Runnable() {
@Override
public void run() {
try {
handleConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
//处理客户端和服务器之间的连接,并且完成数据的接收,处理,回应
public void handleConnection(Socket socket) throws IOException {
System.out.printf("客户端上线 %s %d\n",socket.getInetAddress().toString(),socket.getPort());
//由于TCP协议的网络编程,进行数据传输的基本单位是字节,所以需要inputstream和outputstream类型的对象来处理字节流的数据
//socket网络连接对象可以直接实例化InputStream和OutputStream对象来对网络要传输的字节数据进行处理
//采用try(){}的结构,在()中实例化inputStream和outputStream对象,可以在{}中的程序执行结束了以后自动关闭这两个对象(防止内存泄漏)
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
//处理客户端传来的请求
//客户端传来的请求可能不止一个,所以需要一直死循环去一直读取客户端传来的请求
while (true){
//InputStream直接使用不是很方便,包装一层Scanner使用
Scanner scanner=new Scanner(inputStream);
//当没有读取到请求了(客户端传来的请求读取完了)就跳出循环,结束方法,去拿下一个网络连接对象进行处理
//当客户端还没有传入请求时就会进入阻塞等待,直到客户端传入请求或客户端下线才恢复
if(!scanner.hasNext()){
//只有当客户端下线才会执行这段代码
System.out.printf("%s %d客户端下线",socket.getInetAddress().toString(),socket.getPort());
break;
}
//客户端中还有请求,读取客服端的请求到request字符串中
//这里默认了客户端传来的请求是字符串,按照字符串的形式来处理请求
String request=scanner.next();
//对客户端传来的请求进行处理并作出响应
String response=handle(request);
//将响应传递给客户端
//直接使用outputStream比较麻烦,包装一层PrintWriter进行使用
PrintWriter printWriter=new PrintWriter(outputStream);
//要调用println方法将回应传递给客户端才行,因为println方法在传递了一个回应后会换行,而在客户端中就刚好可以用next方法读取
//(next方法不会读取空白符,而换行属于空白符)
printWriter.println(request);
//由于IO操作很消耗资源,所以在调用println方法向客户端发送数据时会先将数据放到一个内存缓冲区中,等到有一定的内容再一起发送
//所以为了保证回应被及时的发到客户端,就要对内存缓冲区进行刷新
printWriter.flush();
//写日志,方便观察当前运行状态
System.out.printf("[%s,%d] request:%s response:%s\n",socket.getInetAddress().toString(),socket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally { //Socket连接对象会被不停的从serverSocket对象中取出,所以在使用完毕Socket连接对象以后应该调用close方法关闭,否则会出现内存泄漏
socket.close();
}
}
//对客户端传来的请求进行处理
public String handle(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer=new TcpEchoServer(1010);
tcpEchoServer.start();
}
}
客户端代码
package TcpEcho;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: wuyulin
* Date: 2023-08-10
* Time: 19:26
*/
//TCP回显客户端
public class TcpEchoClient {
//客户端需要实例化一个连接对象Socket来实现客户端与服务器之间的数据交互
Socket socket=null;
//在实例化连接对象Socket时需要服务器的ip地址和端口号
TcpEchoClient(String serverIp,int serverPort) throws IOException {
socket=new Socket(serverIp,serverPort);
}
public void start(){
//写日志,方便观察当前运行状态
System.out.println("客户端启动");
//TCP协议进行网络编程传递数据的基本单位是字节,所以需要用到InputStream和OutputStream的对象来进行数据的传输
//通过连接对象即可实例化InputStream和OutputStream的对象
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream();){
//客户端需要不停的读取客户输入的请求,所以要用循环
while (true){
Scanner scanner=new Scanner(System.in);
//读取用户的请求
System.out.println("->");
String request=scanner.next();
//将用户的请求发送给服务器
//直接使用outputStream不方便,包装一层PrintWriter使用
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
//由于IO操作很消耗资源,所以在调用println方法向客户端发送数据时会先将数据放到一个内存缓冲区中,等到有一定的内容再一起发送
//所以为了保证请求被及时的发到服务器,就要对内存缓冲区进行刷新
printWriter.flush();
//接收服务器处理后的回应
//直接使用inputStream不方便,包装一层Scanner使用
Scanner inputScanner=new Scanner(inputStream);
String response=inputScanner.next();
//将回应打印到控制台
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",1010);
tcpEchoClient.start();
}
}
文章来源地址https://uudwc.com/A/ABgRB