本文主要介绍了TCP版本的回显服务器的编写。
一.TCP版本回显服务器
1.服务器
服务器的实现流程
1.接收请求并解析
2.根据请求计算出响应(业务流程)
3.把响应返回给客户端
代码:
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;
/**
* Tcp版本的回显服务器
*
* 服务器
*/
public class TcpEchoServer {
private ServerSocket serverSocket=null;
//使用线程池:此处不应该创建固定线程数目的线程池
private ExecutorService service= Executors.newCachedThreadPool();
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
//这个操作会绑定端口
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//从内核中的连接获取到应用程序中
/**
*
* accept是把内核中已经建立好的连接,给拿到应用程序中,但是这里的返回值并非是
* 一个connection对象,而只是一个socket对象,这个socket对象就像一个耳麦
* 可以说话,也可以听到对方的声音
*/
Socket clientSocket=serverSocket.accept();
//单个线程,不方便完成这里的一边拉客,一边介绍;就需要多线程
//多线程负责拉客
//每次有一个新的客户端,都创建一个新的线程去服务
// Thread t=new Thread(()->{
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
//
// });
// t.start();
//使用线程池也可以解决
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
//通过这个方法来处理一个连接的逻辑
private void processConnection(Socket clientSocket) throws IOException {
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
Scanner scanner=new Scanner(inputStream);
if(!scanner.hasNext()){
//读取完毕,客户端下线
System.out.printf("[%s:%d]客户端下线 \n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
/**
*这里暗含了一个约定,客户端发过来的请求
* 得是文本数据,同时还要包含空白符
*/
String request=scanner.next();
//next一直读到空白符结束(换行,回车,空格,制表符,等)
//2.根据请求计算响应
String response=process(request);
//3.把响应写给客户端
/**
用printWriter把outputstream包裹一下,方便进行收发数据
*/
PrintWriter writer=new PrintWriter(outputStream);
/**
* 使用printWriter的println方法,把响应写给客户端,结尾\n,
* 是为了方便客户端读取响应,使用scanner.next读取
*/
writer.println(response);
/**
* 还需要加一个刷新缓冲区操作
* io操作比较有开销,相比于访问内存,进行io次数越多,程序的速度就越慢
*
* 作为一块内存作为缓冲区,写数据的时候,先写到缓冲区里
* 存一波数据,统一进行io
* printwriter内置了缓冲区
* 手动刷新,确保这里的数据是真的通过网卡发出去了,而不是残留在缓冲区里
*
* 加上flush是更稳妥的做法。
*/
writer.flush();
//打印日志
System.out.printf("[%s:%d] rep:%s , resp:%s \n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,request);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
/**
* socek有很多,每来一个连接,就会有一个连接
*/
//在finally中加上close操作,确保当前socket及时关闭。
clientSocket.close();
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(9090);
server.start();
}
}
说明:
1.循环之后,服务器要做的事情不是读取客户端的请求,而是先处理客户端的连接,因为TCP是面向连接的。
2.一个服务器中,要对应很对客户端,服务器内核中有很多客户端连接。虽然内核中连接很多,但是应用程序还是要一个一个的处理。
我们可以把内核中的连接看成 待办事项, 待办事项在队列中,应用程序需要一个一个完成这些任务
要完成任务,就要先取任务 ; 因此在处理请求之前,要先通过accept()从内核中获得请求。
我们可以把TCP连接的生成和获得连接的过程看作一个生产者消费者模型。
socket中会包含一个管理连接的队列,这个队列是每个socket都有一份,相互之间不会混淆。
3.当服务器执行到accept时,此时如果客户端还没来,accept就会阻塞,直到有客户端连接成功为止。
accept是把内核中已经建立好的连接,拿到应用程序中,返回值是一个socket对象,这个对象就像一个耳麦,既可以说话,也可以听到对反的声音。
也就是通过socket对象就可以和对方进行网络通信
此时这个回显服务器中,涉及到两种socket
1.ServerSocket
相当于是在店外揽客的服务员,揽到客人之后,交给店内的服务员
2.clientSocket
店内负责招待的服务员
4.
scanner和printwriter没有close,并不会导致文件资源暴露
流对象中持有的资源的两个部分
1)内存(对象销毁,内存回收)
2) 文件描述符 scanner和printwriter持有的是inputstream和outpustream的引用
5.服务器怎么感知到客户端下线的
hasNext()在客户端没有发请求的时候,也会阻塞,一直阻塞到客户端发了请求,或者是客户端退出,它就返回了
2.客户端
基本实现流程:
1.从控制台读取用户的输入
2.把输入的内容构造成请求发送给服务器
3.从服务器读取响应
4.把响应显示到控制台上
代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Tcp版本的服务器
*
* 客户端
*/
public class TcpEchoClient {
private Socket socket=null;
//要和服务器通信,就需要先知道,服务器所在的位置
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//这个new操作就完成了tcp连接的建立
socket = new Socket(serverIp, serverPort);
}
private void start() {
System.out.println("客户端启动");
Scanner scannerConsole=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
while(true){
//1.从控制台输入字符串
System.out.print("->");
String request=scannerConsole.next();
//2.把请求发送给服务器
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
/**
* 不要忘记flush
* 确保数据真的发送出去了
*/
printWriter.flush();
//3.从服务器读取响应
Scanner scannerNetwork=new Scanner(inputStream);
String response=scannerNetwork.next();
//4.把响应打印出来
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
二.问题和解决方法
1.服务器问题
1.关闭当前的socket!!放在finally当中
客户端会有很多,而每个客户端都有一个socket,如果不关闭会消耗大量的资源。
2.(重点!上面的代码是修改后的!)
两个以上(包含)客户端发来的请求,服务器无法正确地处理。
这是因为当第一个客户端来了,accept会返回,进入processConnection
在处理这个客户端请求过程中,即使第二个客户端来了,也无法第二次调用accept
解决办法:改进成多线程
主线程:负责accept,和客户端建立连接
然后创建新的线程,让新的线程去处理客户端的各种请求
更好的办法:使用线程池!
这样可以避免频繁创建和销毁线程。