本文主要介绍了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() {@Overridepublic 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.根据请求并解析,为了读取方便,直接使用scannerScanner 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,和客户端建立连接
然后创建新的线程,让新的线程去处理客户端的各种请求
更好的办法:使用线程池!
这样可以避免频繁创建和销毁线程。