Java网络编程:(socket API编程:TCP协议的 socket API -- 回显程序的服务器端程序的编写)

Java网络编程:(socket API编程:TCP协议的 socket API – 回显程序的服务器端程序的编写)

观前提醒

如果你是第一次点击这篇博客进来的,需要你先看完这篇博客,以及它里面提到的博客,再过来看这篇博客:
Java网络编程(4):(socket API编程:TCP协议的 socket API – 回显程序)

1. 准备工作

先构建一个类:TCPEchoServer(这个类,表示服务器程序)
当然,你也可以自己命名。
在这里插入图片描述

1.1 创建 ServerSocket对象

我们编写的是 TCP 协议的程序,使用的是 TCP协议的 socket API,并且这里是对服务器端程序的编写,我们使用的类是:ServerSocket

代码:

private ServerSocket serverSocket = null;

1.2 通过构造方法,初始化 ServerSocket对象

我们通过 TCPEchoServer 这个类的构造方法,在它的构造方法里面,调用 ServerSocket 类的构造方法 ServerSocket(int port),对 Socket 对象,进行端口号的绑定

代码:

    public TCPEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

1.3 创建 start 方法,用来启动服务器

通过start 方法,用来启动服务器。

代码:

    public void start(){
        System.out.println("启动服务器!");
        
        
    }

接下来,就是在 start 方法里面,实现服务器端程序的全部功能。

1.4 编写一个死循环,让服务器不断工作

对于服务器来说,客户端什么时候发请求,发送多少个请求,我们是无法预测的。
例如:
你玩游戏(手机里的游戏APP,就是客户端),游戏对应的服务器端(也就是服务器),是不知道你什么时候登录游戏,发送一个登录游戏的请求的,也不知道,在一个小时内,有多少个玩家发送了多少个登录请求,游戏公司无法预测,所以,游戏的服务器,是 7 X 24 小时,不断工作的,只有游戏版本更新,系统维护的时候,服务器才会关闭。
服务器关闭了,玩家登录游戏的时候,发送的登录请求,服务器关闭了,无法作出响应,你就登录不了游戏了。
只有更新结束,游戏系统维护好了以后,才能正常玩游戏。

通过上述例子可知:服务器端程序(服务器)通常都需要有一个 死循环,做到 7 X 24小时运行。

1.5 结束准备工作

代码:

import java.io.IOException;
import java.net.ServerSocket;


public class TCPEchoServer {

    private ServerSocket serverSocket = null;

    public TCPEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start(){
        System.out.println("启动服务器!");

//        服务器需要不断的工作,用一个死循环实现这一点
        while(true){
            
//            接下来在这里继续编写代码
//            ......
            
        }
        
    }

    
}

2. 建立连接

在while循环中,与UDP那边的服务器端程序不同。
UDP协议那边,你可以直接读取请求数据并解析,根据请求,计算响应,最后将响应返回给客户端了。

但是 TCP协议 不同,需要先处理客户端发来的连接
这两者的差别就是因为,TCP是有连接的,UDP是无连接的。

如何理解 TCP 这么做的原因呢?
你可以理解为打电话

2.1 举例说明,并理解

TCP 先建立连接,好比如我们生活中打电话。

打电话的情景:
我需要打电话给老板汇报工作我(客户端)需要先拨号,给老板的手机(服务器)发送通话请求

我这边发送请求之后,老板的手机就响铃提醒了,有人打电话,是否要接听电话,此时需要老板点击 “ 接通 ”,我才能和老板进行通话。
否则,老板不点击 “ 接通 ”,我这边,说再多东西,都是白搭,因为老板根本就不知道我说了什么,原因就是因为我们两个没有建立连接,无法进行沟通

上述过程,我(打电话那一方)就类似于与客户端,发起了连接的请求,老板(需要点击 “ 接通 ” 电话的那一方),就类似于服务器端,需要与发起连接的客户端,建立连接。

只要服务器端,和客户端先建立连接了,后续客户端发送过来的请求信息,服务器端才能接收的到,并进行处理,最后将响应返回给客户端。

2.2 使用 accept 方法,建立连接

建立连接的操作,我们需要使用到 accept() 方法。

方法名方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回⼀个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待

相当于 客户端 给 服务器端打电话,服务器端调用了 accept方法,就接通了电话,可以进行通信了。

使用我们创建好的 Socket对象,调用 accept方法:
在这里插入图片描述
同时,这个方法还有一个返回值 :Socket

我们需要接收这个返回值:
在这里插入图片描述
这个返回值(clientSocket),是建立连接之后,返回的一个 Socket对象,是后续客户端和服务器端进行通信的一个对象。

后续,就是通过 读写 clientSocket ,和客户端进行通信

可能大家还是对于这两个对象,有点懵,下面举个例子,看看你能不能更好的理解。

2.3 举例理解两个Socket对象的工作职责

不知道大家有没有在街上看到过西装革履的小哥或小姐姐。

一般看到这样的人,大概率是销售,只有少部分是其他。
这个销售可以销售很多东西,我就以卖房为例:

西装革履的销售小哥,在马路街道上,发传单或者口头呼喊着:“ 这里有一个不错的楼盘,快来看看,买到就是赚到… … ” 之类的。
此时,有一位帅小伙刚好想买房,就过去问了问,于是,销售小哥就把他带到了售楼中心,小哥一挥手,又来了一个西装革履的小姐姐,这位小姐姐是专业的销售顾问(也还是销售),接下来就由这位小姐姐给这位帅小伙介绍楼盘的详细情况。
小哥一转身,又出去招揽客人去了。

像这种,小哥在马路街道上,进行揽客,小姐姐,负责给客人提供详细的服务,就是这两个 Socket对象 的工作职责。

在这里插入图片描述
在这里插入图片描述
serverSocket:相当于小哥,在马路街道上招揽客人
多个客户端要进行连接的时候,这个 Socket对象(serverSocket),调用 accept方法,与客户端建立连接,之后,把客人(建立了连接的客户端),交给了小姐姐(clientSocket)。

clientSocket:相当于小姐姐给客人(建立了连接的客户端)提供详细的服务
我们跟客户端进行读写发送,接收数据,就是用过这个 Socket对象(clientSocket),来进行负责的。

2.4 客户端没有发起连接

如果客户端没有发起连接,此时 accept方法 就会阻塞等待

跟 UDP 那边的 receive方法是类似的,如果客户端没有发送请求数据过来服务器端,receive方法,就会阻塞等待。

2.5 阶段总结

这里,我们主要是完成了对客户端的连接建立,后续,可以通过 clientSocket ,来对客户端的请求和响应进行处理了。

代码:

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;


public class TCPEchoServer {

    private ServerSocket serverSocket = null;

    public TCPEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器!");

//        服务器需要不断的读取客户端的请求数据,用一个死循环实现这一点
        while(true){
            // 对于 TCP 来说,需要先处理客户端发来的连接
            // clientSocket 这个变量,就表示和服务器和客户端进行交流的一个对象,
            // 后续,就通过读写 clientSocket ,和客户端进行通信
            Socket clientSocket = serverSocket.accept();
            

        }

    }


}

3. 处理客户端请求和响应前的准备工作

与客户端建立连接之后,我们就可以对客户端的请求和响应,进行处理了。

但是,在那之前,我们仍要做一些准备工作:
处理的过程,封装成一个方法
然后打印日志,说明客户端已经连接成功了,
最后获取两个流对象,InputStream 和 OutStream。

3.1 单独封装成一个方法

当我们和一个客户端建立连接之后,就要对这个客户端的请求和响应,进行处理,这是一个单独的功能模块了。

对于一个单独的功能模块,建议用另一个类,或者封装成一个方法,来进行代码的编写,这样以后程序维护起来比较方便。

这个程序,对于客户端的请求和响应的处理过程,我们封装成一个方法:processConnect(clientSocket)
在这里插入图片描述
因为后续对客户端的读写,都是通过 clientSocket,所以,把它当作参数,传递给这个方法。

所以,这个方法,就是处理一个客户端连接之后的工作
一个客户端建立连接之后的操作,可能涉及到这个客户端很多个的请求和响应。

因为一个客户端,建立连接之后,并不一定只发一个请求,有可能是多个请求,我们也要处理请求,得到多个响应

3.2 打印日志

我们可以在 processConnect(clientSocket) 方法中,打印一个日志,输出 clientSocket 的 IP 和 端口号,判断客户端是否已经建立连接了。

打印的方式,通过 C语言 中的 printf 方式进行打印:

//        打印一个日志,输出 clientSocket 的 IP和端口号
        System.out.printf("[%s : %d] 客户端连接成功,客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());

这里,我们使用 Socket 提供的 getInetAddress() 方法,获取到客户端的 IP地址

方法名方法说明
InetAddress getInetAddress()返回套接字所连接的地址

clientSocket.getPort(),是获取到客户端的端口号。

3.3 获取两个流对象

TCP协议 并没有像 UDP协议 那里一样,提供了send 和 receive 方法。
而是提供了InputStream getInputStream()OutputStream getOutputStream()这两个方法,获取到 字节输入流对象字节输出流对象

Socket 本身,是不会进行读写操作的,而是借助两个字节流对象,InputStream 和 OutStream,完成类似于 UDP协议的 send 和 receive 完成的工作。

所以,想要处理请求,得到响应,我们就需要获取到字节输入流对象 和 字节输出流对象(InputStream 和 OutputStream)。

try with resource 语法说明

Java 文件操作 和 IO(3)-- Java文件内容操作(1)-- 字节流操作 这篇博客中,我们介绍了一种定义 字节流对象 的方式:try with resource语法。

try with resource 语法,就是把需要进行关闭的资源(需要执行关闭操作 close方法 的类),放到 try 后面的括号里面,只要出了 try 的代码块,就会自动调用该资源的 close方法。
这个语法,不仅可以达到关闭文件资源的操作,而且,也简化了代码,使代码看起来更加美观!

更多细节,点击上面的博客连接,进行了解。

编写代码

在这里插入图片描述

代码:

        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            


        } catch (IOException e) {
            throw new RuntimeException(e);
        }

拿到输入流对象(InputStream )和输出流对象(OutputStream )
之后,我们读客户端的请求的时候,我们使用输入流对象(InputStream
返回响应给客户端的时候,我们使用输出流对象(OutputStream ),把响应写回给客户端

之后,我们就在 try 代码块里面,开始真正处理客户端的请求,并得到响应。

3.4 阶段总结

这里我们主要是在处理客户端的请求和响应之前,做了些准备工作,包括:

  • 处理的过程,封装成一个方法
  • 打印日志,检测客户端是否已经连接成功
  • 获取两个流对象,InputStream 和 OutStream

代码:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;


public class TCPEchoServer {

    private ServerSocket serverSocket = null;

    public TCPEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器!");

//        服务器需要不断的读取客户端的请求数据,用一个死循环实现这一点
        while(true){
            // 对于 TCP 来说,需要先处理客户端发来的连接
            // clientSocket 这个变量,就表示和服务器和客户端进行交流的一个对象,
            // 后续,就通过读写 clientSocket ,和客户端进行通信
            Socket clientSocket = serverSocket.accept();
//            处理一个客户端的请求和响应
            processConnect(clientSocket);



        }

    }

//    处理一个客户端的连接
//    一个客户端,可能会发送多个请求,就要处理多个请求,得到多个响应
    private void processConnect(Socket clientSocket) {
//        打印一个日志,输出 clientSocket 的 IP和端口号
        System.out.printf("[%s : %d] 客户端连接成功,客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());

        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){

//            真正处理客户端的请求,并得到响应
//            ......

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

}

4. 处理请求,得到响应

在上面我们说过:因为一个客户端,建立连接之后,并不一定只发一个请求,有可能是多个请求,我们也要处理请求,得到多个响应。

处理请求的步骤分为三步,但是,处理请求的这三个步骤,一次只能处理一个请求,如果是一个客户端,有多个请求,就需要多次处理,需要使用一个循环,处理多个请求。

并且,这个循环是一个死循环,因为你不知道客户端到底会发多少个请求过来。

代码:
在这里插入图片描述

处理请求的步骤

接下来的步骤,和 UDP 那边,处理请求的步骤是一样的了。

处理请求的过程,典型的服务器都是分为三个步骤的,分别是:

  1. 读取请求,并解析请求数据
  2. 根据请求,计算响应(服务器最关键的逻辑)

本身服务器就是要提供各种各样的功能的,比如:百度搜索 “ 如何学习好Java ”,我的请求是:“ 如何学习好Java ”,服务器端的程序就会根据请求,搜索出很多个相关的页面。

说起来很简短,但是,在服务器端程序上,会经过各种各样的逻辑处理,处理请求,计算响应,最终返回结果。
所以,这一块是服务器最关键的逻辑。

但是,我们这篇博客编写的是回显服务器,所以,这个环节就相当于省略了,请求是什么,我们就返回什么。

  1. 把响应返回给客户端

所以,一个典型的服务器,都是按照这三个步骤来进行处理的,不仅是我们编写的这个回显服务器,未来,你从事编写服务器端程序(服务器)相关的工作岗位,涉及到更加复杂的,商业级别的服务器,总体上的逻辑,也就是这三步。
只不过,公司的服务器端程序,会考虑的更多,支持的功能会更多,会很复杂,上至几十万行的代码量。

接下来,我们就根据这三个步骤,进行代码的编写。
在这里插入图片描述

由于篇幅太长了,我将处理请求的这三个步骤的代码编写,放到一个新的博客里面,大家需要点击进去这里看:

Java网络编程:(socket API编程:TCP协议的 socket API – 服务器端处理请求的三个步骤)

5. 为 TCPEchoServer类(服务器端程序),编写main方法,启动服务器端程序

在这里插入图片描述

代码:

    public static void main(String[] args) throws IOException {
        TCPEchoServer tcpEchoServer = new TCPEchoServer(9090);
        tcpEchoServer.start();
    }

6. 总结编写步骤

看完上面的步骤,可能你还有点懵,我来带你会看一下,这个程序,整一个的编写流程:

1. 准备工作

这一步,我们主要是为了后续的编写,做一些准备工作。

准备工作有:

  1. 创建服务器端的 ServerSocket对象,并通过 TCPEchoServer 这个服务器类的构造方法,绑定服务器端程序的端口
  2. 创建 start 方法,让实例化的对象,调用这个方法,启动服务器程序。
  3. 为了让服务器不断的工作,在 start 方法中,创建一个死循环

2. 和客户端建立连接

这个时候,是启动服务器后,服务器进行工作,需要做的事情,所以,代码应该是在 start方法 中的死循环中编写。

建立连接:
通过 服务器端的 ServerSocket对象,调用 accept方法,和客户端建立连接,并接收返回值。

3. 处理客户端请求之前的准备工作

这一步,是和客户端建立连接之后,客户端回发送请求过来之前,我们需要做准备工作,然后再进行请求的处理。

准备工作:

  1. 处理客户端发送的请求,是一个独立的功能模块,用一个单独的方法封装一下,此处是自定义的,如: processConnect(clientSocket);
  2. 打印一个日志,输出 clientSocket(可以理解为建立连接之后,返回的客户端的代表) 的 IP 和 端口号,判断客户端是否已经建立连接了。
  3. 通过 try with resource 语法获取 clientSocket 的两个流对象,后续通过他们,读取客户端发送过来的请求 和 返回响应给客户端。

4. 处理请求,得到响应

从这一步开始,我们就可以根据处理请求的三个步骤,进行代码的编写了。

由于与客户端建立连接之后,一个客户端可能发送多个请求,需要建立一个死循环,不断处理发送过来的请求,直到下一次没有请求发送过来了,就结束死循环,也断开和客户端的连接。
并且,处理请求的三个步骤的代码,也在这个死循环里面写。

代码编写:

  1. 读取请求,并解析(以下两种,二选一)
  • 可以使用 InputStream 提供的 read 方法读取数据字节流的方式),然后手动转化为 String,方便后续操作
  • 也可以通过 Scanner 的方式(字符流的方式),进行读取。
  1. 根据请求,计算响应
    回显程序:客户端发送的请求,就是返回给客户端的响应
  2. 将响应返回给客户端(以下两种,二选一)
  • 使用 OutputStream 提供的 write方法,将响应写到客户端(返回给客户端),这是 字节流的方式。
  • 也可以使用 PrintWriter 将响应写回(发送)给客户端,这是字符流的方式

5. 为 TCPEchoServer类(服务器端程序),编写main方法,启动服务器端程序

编写main方法,创建一个 TCPEchoServer类 的对象,调用 start方法,启动服务器端程序。

6. 目前的服务器端代码:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TCPEchoServer {

    private ServerSocket serverSocket = null;

    public TCPEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器!");

//        服务器需要不断的读取客户端的请求数据,用一个死循环实现这一点
        while(true){
            // 对于 TCP 来说,需要先处理客户端发来的连接
            // clientSocket 这个变量,就表示和服务器和客户端进行交流的一个对象,
            // 后续,就通过读写 clientSocket ,和客户端进行通信
            Socket clientSocket = serverSocket.accept();
//            处理一个客户端的请求和响应
            processConnect(clientSocket);



        }

    }

//    处理一个客户端的连接
//    一个客户端,可能会发送多个请求,就要处理多个请求,得到多个响应
    private void processConnect(Socket clientSocket) {
//        打印一个日志,输出 clientSocket 的 IP和端口号
        System.out.printf("[%s : %d] 客户端连接成功,客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());

        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
//            针对 InputStream,采用 Scanner 进行读取操作,套了一层
            Scanner scanner = new Scanner(inputStream);
//            针对 OutputStream ,采用 PrintWriter 进行写操作,套了一层
            PrintWriter writer = new PrintWriter(outputStream);
//            一个客户端,可能会发送多个请求,就要处理多个请求,得到多个响应
//            所以,需要使用 while 循环,处理多次
//            不知道客户端到底会发送多少个请求,所以,使用死循环
            while(true){
//            这里的工作分为三个步骤:
//                1. 读取请求,并解析请求数据

//                字节流的方式,进行读取操作
//                byte[] requestByte = new byte[1024];
//                int byteNum = inputStream.read(requestByte);
//////                为了后续的处理更加方便,需要手动转换为 String
//                String request = new String(requestByte,0,byteNum);


//                对 InputStream 进行套壳,使用 Scanner 的方式,进行读取
                if (!scanner.hasNext()){
//                    客户端没有请求发送过来了,就不需要继续读取请求,并处理了
//                    可以断开和客户端的连接(结束循环)

//                    打印一个日志,显示 服务器 和 客户端,断开了连接
                    System.out.printf("[%s : %d] 断开连接,客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
//                使用 Scanner 读取请求
                String request = scanner.next();


//                2. 根据请求,计算响应(服务器最关键的逻辑)
                String response = precess(request);


//                3. 把响应返回给客户端

//                字节流的方式,将响应写回(发送)给客户端
//                使用字节输出流对象提供的 write方法,将响应写到客户端(返回给客户端)
//                outputStream.write(response.getBytes());
//                OutputStream是以字节为单位,进行写操作的
//                response.getBytes():当前响应是一个String,需要拿到响应中的字节数据,才能进行写操作。

//                对 OutputStream 进行套壳,使用 PrintWriter 的方式,将响应写回(发送)给客户端
                writer.println(response);


//                4.打印日志,显示 客户端发送的请求 和 服务器端返回的响应信息
                System.out.printf("[%s : %d] 请求(req):%s,响应信息(resp):%s \n",clientSocket.getInetAddress(),clientSocket.getPort(),
                                    request,response);

            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private String precess(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TCPEchoServer tcpEchoServer = new TCPEchoServer(9090);
        tcpEchoServer.start();
    }
}

7. 总结

这篇博客,完成了对服务器端程序代码的编写,但是,当我们配合客户端一起运行的时候,会有问题

这个问题是使用了 PrintWriter 方式后,才会产生的问题
关于缓冲区的问题。

怎么回事?
我们在这篇博客中解决:
Java网络编程(4):(socket API编程:TCP协议的 socket API – 回显程序)

最后,如果这篇博客能帮到你的,请你点点赞,有写错了,写的不好的,欢迎评论指出,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值