JavaEE - 网络编程

文章介绍了网络编程的基础知识,包括为什么需要网络编程以及网络编程的基本概念。接着详细讲解了Socket套接字,特别是TCP和UDP的区别,如连接方式、传输可靠性、数据传输模式等。然后通过示例展示了如何使用UDP进行数据报套接字编程,最后提到了TCP流套接字编程,并给出了回显服务的TCP版实现,强调了多线程在处理多个客户端连接时的重要性。
摘要由CSDN通过智能技术生成

一、网络编程基础

为什么需要网络编程?
用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。
在这里插入图片描述
与本地打开视频文件类似,只是视频文件这个资源的来源是网络。
相比本地资源来说,网络提供了更为丰富的网络资源:
在这里插入图片描述
所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的。

1.1、什么是网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。

在这里插入图片描述

当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。

特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。

但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:

  • 进程A:编程来获取网络资源
  • 进程B:编程来提供网络资源

1.2、网络编程中的基本概念

发送端和接收端
在一次网络数据传输时:

  • 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
  • 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
  • 收发端:发送端和接收端两端,也简称为收发端。

注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
在这里插入图片描述

请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:

  • 第一次:请求数据的发送
  • 第二次:响应数据的发送。

好比在快餐店点一份炒饭:
先要发起请求:点一份炒饭,再有快餐店提供的对应响应:提供一份炒饭
在这里插入图片描述

客户端和服务端

  • 服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
  • 客户端:获取服务的一方进程,称为客户端。

对于服务来说,一般是提供:
客户端获取服务资源
在这里插入图片描述
客户端保存资源在服务端
在这里插入图片描述
好比在银行办事:

  • 银行提供存款服务:用户(客户端)保存资源(现金)在银行(服务端)
  • 银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管的现金)

常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:

  1. 客户端先发送请求到服务端
  2. 服务端根据请求数据,执行相应的业务处理
  3. 服务端返回响应:发送业务处理结果
  4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

在这里插入图片描述

二、Socket套接字

Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。

2.1、Socket套接字分类

Socket套接字主要针对传输层协议划分为如下三类:

  • 1、流套接字:使用传输层TCP协议
    TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
    以下为TCP的特点(细节后续再学习)
    有连接
    可靠传输
    面向字节流
    有接收缓冲区,也有发送缓冲区
    大小不限

对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情
况下,是无边界的数据,可以多次发送,也可以分开多次接收。

  • 2、数据报套接字:使用传输层UDP协议
  • UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
    以下为UDP的特点(细节后续再学习):
    无连接
    不可靠传输
    面向数据报
    有接收缓冲区,无发送缓冲区
    大小受限:一次最多传输64k

对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一
次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节

  • 3、原始套接字
    原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
    我们不学习原始套接字,简单了解即可。

在这里插入图片描述
接下来先来了解TCP和UDP两种协议

2.2、TCP 与 UDP 的 区别

在这里插入图片描述

有连接 和 无连接

可以怎么去理解:
有链接:像打电话

比如说:现在我们要打电话给某个朋友。
输入号码,按下手机拨号键。
手机开始发出 嘟嘟嘟 声音,开始等待对方接听,

而且,我们拨号之后,并不是马上就能接通的!
必须要等待 对方接听之后,我们才能与其交流。

之所以说:有链接 就像 打电话一样,是因为 打电话,必须要接通了之后,才能交流;没有接通,双方就无法交流。
有连接的意思:就是在两者确认建立联系后,就可以开始交互了。

无连接:发微信

不需要接通,直接就能发数据。
发微信,我们都知道:发送信息的时候,是不需要对方在线或者回复,按下回车,立马就能加个信息发送出去,不过 对方 看没看见这条消息,我们是不确定的 。
这种情况,就叫做无连接。

所以 TCP,就是要求双发先建立连接,连接好了,才能进行传数据。
而 UDP,直接传输数据,不需要双方建立连接。

可靠传输 和 不可靠传输

可靠传输:发送方 知道 接收方 有没有接收到数据

注意!不要理解错了。
可靠传输,不是说数据发送之后,对方100% 就能收到。
你代码写得再好,也刚不住挖掘机把你家网线挖断了。
网线都断了,你能把数据发出去才有鬼。

可靠传输,不是说传输数据百分百成功,关键还得看这里面是否能感知到 传输数据成功了。

关于可靠传输,还有一种错误理解。
可靠传输,就是“安全传输”。这种说法也是一个典型的错误。
可靠 和 安全 是 两码事!!!!

安全,指的是 数据在传输过程,不容易被黑客窃取,不容易被篡改。
可靠,指的是 数据发给对方,发送方能知道接收方有没有收到数据。
在这里插入图片描述

不可靠传输:发送方 不知道 接收方有没有接收到数据
在这里插入图片描述

总得来说:
可靠,就是我们对于自己发送的信息,心里有点数。
心里没底,就是不可靠。

面向字节流 与 面向数据报

面向字节流:数据是以字节为单位,进行传输的。

这个就非常类似于 文件操作中的文件内容相关的操作 中的字节流。
网络传输也是一样!
假设,现有100个字节的数据。
我们可以一直发完。
也可以 一次发 10个字节,发送十次。
也可以 一次发 2 个字节,发送50次。

面向数据报:
以数据报为单位,进行传输。

一个数据报都会明确大小。
一次 发送/接收 必须是 一个 完整的数据报。
不能是半个,也不能是一个半,必须是整数个。

在代码中,这两者的区别是非常明显的!

全双工

全双工 对应的是 半双工

全双工:一条链路,双向通信。

举个例子:间谍
通常抓到一个间谍,都会对其进行拷问。
说:你的上级是谁?平时是怎么联系的?
间谍:我和他认识,知道彼此身份,并且有相互联系的方式。
      他是xxx,联系方式xxxxxx。所以别再打我,作用不大,因为我都会说。

半双工:一条链路,单向通信。

举个例子:间谍
通常抓到一个间谍,都会对其进行拷问。
说:你的上级是谁?平时是怎么联系的?
间谍:我和上级是单向通信的,他联系到我,我联系不到他。所以别再打我,作用不大

TCP 和 UDP 都是全双工。
半双工理解即可。

三、UDP数据报套接字编程

在这里插入图片描述

3.1、DatagramSocket API

DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。

DatagramSocket 构造方法
方法签名方法说明
DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(intport)创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
DatagramSocket 方法
方法签名方法说明
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p)从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()关闭此数据报套接字

3.2、DatagramPacket API

DatagramPacket 是 UDP Socket 发送和接收的数据报。

DatagramPacket 构造方法
方法签名方法说明
DatagramPacket(byte[] buf, int length)构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号
DatagramPacket 方法
方法签名方法说明
InetAddress getAddress()从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData()获取数据报中的数据

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。

3.3、InetSocketAddress API

InetSocketAddress ( SocketAddress 的子类 )构造方法:

方法签名方法说明
InetSocketAddress(InetAddress addr, int port)创建一个Socket地址,包含IP地址和端口号

3.4、示例一:写一个最简单的客户端服务器程序【回显服务】

什么是回显服务?
回显服务 - Echo Server
简单来说,我说什么,你回什么。

就像回声,重复着我们说过话。

回显服务,就是这样的。
我们发送什么样子的数据,它就给我们返回一个同样的数据。
也就是说:根据我们请求的内容数据,来返回一个具有相同数据的响应。

这样的程序属于最简单的网络编程中的程序。
因为不涉及到任何的业务逻辑,就只是通过 socket API 进行单纯的数据转发。
我通过这个程序,来向大家演示 API 的使用。

准备工作
在这里插入图片描述

服务器部分
在这里插入图片描述

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

/**
 * Created with Intellij IDEA.
 * Description:
 * User: ryy
 * Date: 2023-05-02
 * Time: 16:28
 */
public class UdpEchoServer {
    //进行网络编程,首先要准备 socket 实例
    private DatagramSocket socket = null;

    //port 是服务器的端口号
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        //UDP是不需要建立连接的,直接接收客户端发来的数据即可。
        while (true){
            //1、读取客户端发来的请求
            //为了接受数据,我们需要先准备好一个空的DatagramPacket对象.
            //然后,由receive 来填充数据。
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(requestPacket);

            //把 DatagramPacket 对象构造出一个字符串
            String request = new String(requestPacket.getData(),0, requestPacket.getLength(),"UTF-8");
            //2、根据请求计算响应(由于咱们这是一个回显服务,这一步就可以省略了)
            String response = process(request);

            //3、把响应写回客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);

            // 打印具体 IP、端口、请求、响应
            System.out.printf("[%s:%d] request: %s,response: %s\n",
                    requestPacket.getAddress().toString(),// 客户端IP
                    requestPacket.getPort(),// 客户端端口号
                    request,//请求
                    response);// 响应
        }
    }

    //由于是 回显服务,响应和请求是一样的
    private String process(String request) {
        return request;
    }

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

运行效果
在这里插入图片描述

客户端部分
在这里插入图片描述

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

/**
 * Created with Intellij IDEA.
 * Description:
 * User: ryy
 * Date: 2023-05-02
 * Time: 16:28
 */
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    public UdpEchoClient(String ip, int port) throws SocketException {
        //此处的 serverPort 是服务器的端口
        // 服务器启动的时候,不需要 socket来指定窗口,客户端自己的端口是系统随机分配的。
        socket = new DatagramSocket();
        serverIP = ip;
        serverPort = port;
    }

    //启动客户端
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true){
            //1、先从控制台上读取用户输入的字符串
            System.out.println("用户输入字符:");
            String request = scanner.next();
            //2、把用户输入的内容,构造成一个 UDP 并发送
            //   构造的请求包括两个部分
            //   1)数据的内容 -》 request
            //   2)要发给谁,服务器的 IP + 端口号
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIP),serverPort);
            socket.send(requestPacket);
            //3、从服务器读取响应数据,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
            //4、把解析的内容显示到控制台
            System.out.printf("[%s:%d] request: %s,response: %s\n",
                serverIP,// 服务器IP
                serverPort,// 服务器端口
                request,//请求
                response);// 响应
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

在这里插入图片描述

客户端 和 服务器交互效果
在这里插入图片描述
这才是我们服务器正常的运行状态,同时处理多个客户端发送的请求。

再来写一个简单程序:就是上面代码的基础上,带上点业务逻辑

写一个翻译程序(英译汉)
请求是一些简单的英文单词。
响应是 英文单词 对应的 中文翻译。
客户端不变,把服务器代码进行调整。
主要是调整 process 方法。
其他步骤都是一样的。
关键的逻辑就是“根据想求来处理响应”
在这里插入图片描述

package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

/**
 * Created with Intellij IDEA.
 * Description:
 * User: ryy
 * Date: 2023-05-02
 * Time: 22:02
 */
 // 创建一个类 UdpDictionaryServer 来表示  字典服务器
// 因为代码的逻辑几乎是一样,且所有的办法都是public的
// 所以我们在这里就直接继承,就可以使用内部所有的方法,并且可以进行重写操作。
public class UdpDictionaryServer extends UdpEchoServer{
    private Map<String,String> map = new HashMap<>();
    public UdpDictionaryServer(int port) throws SocketException {
        super(port);
        map.put("dog","小狗");
        map.put("cat","小猫");
        map.put("duck","小鸭子");

    }

    @Override
    public String process(String request) {
    	// 如果查询的单词在“词库”中存在,就返回其 键值对/对应的中文,
        //反之,如果查询的单词在 “词库”中 不存在,返回 没有对应的词。
        return map.getOrDefault(request,"该词没有翻译!");
    }

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

代码运行结果
在这里插入图片描述

总结

一个服务器,最关键的逻辑就是“根据想求来处理响应”!
什么样的请求,得到什么样的响应。
这是我们一个服务器要完成的一个最最关键的事情。
通过这个东西,才能让我们的程序真正帮我们解决一些实际问题。
这一点,大家要体会我们 服务器-客户端 的交互过程。

之所以,网络编程 是一个 服务器-客户端的结构,是因为 有些工作,我们希望让服务器完成一些工作,既然要完成这样的工作,就得有输入(请求),也有输出(响应)。
从输入到输出,从请求到响应的这个过程,这就是服务器要完成的基本工作。

四、TCP流套接字编程

TCP 和 UDP 的差别很大!
在 TCP API 中,也是涉及到两个核心的类
在这里插入图片描述

4.1、ServerSocket API

4.1.1、ServerSocket 构造方法

ServerSocket 是创建TCP服务端Socket的API。

方法签名方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

4.1.2、ServerSocket 方法

方法签名方法说明
Socketaccept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close()关闭此套接字

4.3、Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

4.3.1、Socket 构造方法

方法签名方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

4.3.2、Socket 方法

方法签名方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

4.4、回显服务器 - TCP版

TCP服务器实现
在这里插入图片描述



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;

/**
 * Created with Intellij IDEA.
 * Description:
 * User: ryy
 * Date: 2023-05-02
 * Time: 22:31
 */
public class TcpEchoServer {
    // listen 的 中文意思是 监听
    // 但是,在Java socket 中是体现不出来 “监听”的含义
    // 之所以这么叫,其实是 操作系统原生的 API 里有一个操作叫做 listen
    // 而 ServerSocket 确实起到了一个监听的效果
    // 所以,取个 listenSocket 的名字
    private ServerSocket listenSocket = null;

    public TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        while (true){
            //注意,UDP 是无连接,所以一上来就发送数据报
            //而TCP是有连接的,要先建立连接,连接好之后才能发送数据
            //accept 就是用来建立连接的,如果没有客户端建立连接,那服务器就会阻塞
            //accept 返回了一个Socket 对象,称为 clientSocket ,后续和客户端的沟通都是通过 clientSocket 来完成的
            //换句话说,ServerSocket 就干了一件事, 建立连接
            Socket clientSocket = listenSocket.accept();

            //处理连接之后的客户端的请求
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        //先打印IP 和 端口号
        System.out.printf("[%s : %d]  客户端建立连接!",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        //接下来就是处理响应和请求
        //此处 处理 TCP Socket 文件读写 和 普通文件的读写是一样的
        try(InputStream inputStream = clientSocket.getInputStream()){
            try (OutputStream outputStream = clientSocket.getOutputStream()){
                //循环处理每个请求
                Scanner scanner = new Scanner(inputStream);
                while (true){
                    if(!scanner.hasNext()){
                        System.out.printf("[%s : %d] 客户端断开连接!",clientSocket.getInetAddress().toString()
                                ,clientSocket.getPort());
                        break;
                    }
                    //此处使用 Scanner 更方便
                    // 如果不用 Scanner,而使用原生的 InputStream 的 read 也是可以的。
                    // 但是很麻烦!它需要构造一个 字节数组 来存储 read 读取的数据。
                    //  read 还会返回字节的个数,如果为-1,即为没有后续数据了,读完了。
                    String request = scanner.next();

                    //2、根据请求 计算响应
                    String response = process(request);
                    //3、把响应返回给客户端
                    //为了方便 用 PrintWriter 把 outputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.print(response);

                    //刷新缓冲区
                    printWriter.flush();
                    System.out.printf("[%s : %d]  request -》 %s  response -》 %s\n",clientSocket.getInetAddress().toString()
                            ,clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //此处用来关闭文件
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


    }

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

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

TCP客户端实现



import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * Created with Intellij IDEA.
 * Description:
 * User: ryy
 * Date: 2023-05-03
 * Time: 21:36
 */
public class TcpEchoClient {
    // 用普通的 Socket 即可,不用 ServerSocket 了
    private Socket socket = null;

    //此处也不用手动给客户端指定端口号,由系统自动分配(隐式)
    public TcpEchoClient(String serverIP,int serverPort) throws IOException {
        // 其实这里是可以给定端口号的,但是这里给了之后,含义是不同的。
        // 这里传入的 IP 与 端口号 的 含义: 表示的不是自己绑定,而是表示 和 这个IP 端口 建立连接
        // 这里表示 与 IP 为serverIP的主机上的 端口为9090的程序,建立连接。
        socket = new Socket(serverIP,serverPort);
    }

    private void start(){
        System.out.println("和服务器建立连接成功!");
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream()){
            try (OutputStream outputStream = socket.getOutputStream()){
                while (true){
                    //客户端还是4个步骤
                    //1、从控制台上输入字符串
                    System.out.println("请输入请求:");
                    String request = scanner.next();
                    //2、把字符串构造成请求,然后发送请求给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush();
                    //3、服务器响应请求,并解析
                    Scanner scaResponse = new Scanner(inputStream);
                    String response = scaResponse.next();
                    //4、将响应显示到控制台上
                    System.out.printf("request:%s,response:%s\n",request,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();
    }
}

此是TCP版本的回显服务写完了,但是还会出现一些问题,UDP版本的可以多个客户端与服务器建立连接,但是TCP就不行,下面我们看一下出现的问题

多线程版本的回显服务器程序

客户端代码和上面一样,只需要改一下服务器里面的代码

当前的服务器,同一时刻只能处理一个客户端连接。
作为一个服务器应该给很多客户端提供服务,而这里只能处理一个客户端,这显然是不科学的。

在这里插入图片描述

public class TcpThreadEchoServer {
    // listen 的 中文意思是 监听
    // 但是,在Java socket 中是体现不出来 “监听”的含义
    // 之所以这么叫,其实是 操作系统原生的 API 里有一个操作叫做 listen
    // 而 ServerSocket 确实起到了一个监听的效果
    // 所以,取个 listenSocket 的名字
    private ServerSocket listenSocket = null;

    public TcpThreadEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        while (true){
            //注意,UDP 是无连接,所以一上来就发送数据报
            //而TCP是有连接的,要先建立连接,连接好之后才能发送数据
            //accept 就是用来建立连接的,如果没有客户端建立连接,那服务器就会阻塞
            //accept 返回了一个Socket 对象,称为 clientSocket ,后续和客户端的沟通都是通过 clientSocket 来完成的
            //换句话说,ServerSocket 就干了一件事, 建立连接
            Socket clientSocket = listenSocket.accept();

            //处理连接之后的客户端的请求
            //改进方法 - 就是在客户端建立连接的时候,创建一个线程执行这里的 processConnection 方法
            Thread t = new Thread(() -> {
                processConnection(clientSocket);

            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        //先打印IP 和 端口号
        System.out.printf("[%s : %d]  客户端建立连接!\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        //接下来就是处理响应和请求
        //此处 处理 TCP Socket 文件读写 和 普通文件的读写是一样的
        try(InputStream inputStream = clientSocket.getInputStream()){
            try (OutputStream outputStream = clientSocket.getOutputStream()){
                //循环处理每个请求
                Scanner scanner = new Scanner(inputStream);
                while (true){
                    if(!scanner.hasNext()){
                        System.out.printf("[%s : %d] 客户端断开连接!\n",clientSocket.getInetAddress().toString()
                                ,clientSocket.getPort());
                        break;
                    }
                    //此处使用 Scanner 更方便
                    // 如果不用 Scanner,而使用原生的 InputStream 的 read 也是可以的。
                    // 但是很麻烦!它需要构造一个 字节数组 来存储 read 读取的数据。
                    //  read 还会返回字节的个数,如果为-1,即为没有后续数据了,读完了。
                    String request = scanner.next();

                    //2、根据请求 计算响应
                    String response = process(request);
                    //3、把响应返回给客户端
                    //为了方便 用 PrintWriter 把 outputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);

                    //刷新缓冲区
                    printWriter.flush();
                    System.out.printf("[%s : %d]  request -》 %s  response -》 %s\n",clientSocket.getInetAddress().toString()
                            ,clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //此处用来关闭文件
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


    }

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

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

效果图
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Später321

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值