【Java网络编程】 三

本文主要介绍了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连接的生成和获得连接的过程看作一个生产者消费者模型。

【Java网络编程】 三_第1张图片

socket中会包含一个管理连接的队列,这个队列是每个socket都有一份,相互之间不会混淆。


3.当服务器执行到accept时,此时如果客户端还没来,accept就会阻塞,直到有客户端连接成功为止。

accept是把内核中已经建立好的连接,拿到应用程序中,返回值是一个socket对象,这个对象就像一个耳麦,既可以说话,也可以听到对反的声音。

也就是通过socket对象就可以和对方进行网络通信


此时这个回显服务器中,涉及到两种socket

【Java网络编程】 三_第2张图片

1.ServerSocket

相当于是在店外揽客的服务员,揽到客人之后,交给店内的服务员

2.clientSocket

店内负责招待的服务员

4.

scanner和printwriter没有close,并不会导致文件资源暴露

流对象中持有的资源的两个部分

1)内存(对象销毁,内存回收)

2)   文件描述符  scanner和printwriter持有的是inputstream和outpustream的引用

5.服务器怎么感知到客户端下线的

【Java网络编程】 三_第3张图片

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,和客户端建立连接

然后创建新的线程,让新的线程去处理客户端的各种请求

更好的办法:使用线程池!

这样可以避免频繁创建和销毁线程。

你可能感兴趣的:(Java,EE初阶,Java网络原理,1024程序员节)