网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔

文章目录

  • 网络编程套接字
    • TCP 与 UDP 的 区别
      • 有连接 与 无连接
      • 可靠传输 和 不可靠传输
      • 面向字节流 与 面向数据报
      • 全双工
    • 小结
  • UDP数据报 套接字编程
    • UDP Socket(套接字)编程中,主要涉及 两个类。
      • UDP协议 - DatagramSocket 的核心方法
    • 实战 : 写一个最简单的客户端服务器程序【回显服务】
      • 服务器部分
        • 构造一个 socket 对象
        • 启动服务器
      • 回显服务器总程序
      • 客户端实现
        • 构造一个 socket 对象
        • 启动客户端
      • 回显客户端总程序
    • 服务器 和 客户端的交互效果图
    • 再来写一个简单程序:就是上面代码的基础上,带上点业务逻辑
      • 字典服务器总程序
        • 效果图
    • 总结
  • TCP流套接字编程
    • 实战:回显服务 - TCP版本
      • 服务器实现
        • 构造一个 ServerSoceket 对象
        • 启动服务器程序 - 与 UDP 差别,尽体现于此部分
      • TCP服务器总程序
      • TCP客户端实现
        • 构造一个 socket对象
        • 启动客户端程序
      • 客户端总程序
      • 效果图
        • 细节拓展:多个客户端 与 服务器建立连接 - 其实为了指出上述代码的缺陷。
          • TCP 服务器优化版 - 多线程版本
        • 效果图
        • 拓展1:TCP 服务器 - 线程池版本
        • 拓展2:TCP 线程池版本服务器 - 带业务:字典
        • 拓展3:细节问题

网络编程套接字

网络编程套接字,是操作系统给应用程序提供的一组API。
这组API,叫做 socket API。
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第1张图片
socket 可以视为是应用层 和 传输层 的 桥梁.

应用层 和 传输层 之间,交互数据就是靠的 socket API。


在上篇网络初识博文中,我们在讲封装的时候,就提到过 传输层的协议有两种 TCP 和 UDP 。
故,socket API 也有对应的两组。

由于我们的 TCP 和 UDP 协议,差别非常很大。
因此,这两组 API 差别也很大。


在正式介绍 这些 API 之前,我们先来了解 TCP 与 UDP 的 区别。


TCP 与 UDP 的 区别

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第2张图片


有连接 与 无连接

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

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

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

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


无连接:发微信

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

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


可靠传输 和 不可靠传输

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

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

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


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

安全,指的是 数据在传输过程,不容易被黑客窃取,不容易被篡改。
可靠,指的是 数据发给对方,发送方能知道接收方有没有收到数据。
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第3张图片


不可靠传输:发送方 不知道 接收方有没有接收到数据。
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第4张图片


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


面向字节流 与 面向数据报

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

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


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

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


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


全双工

全双工 对应的是 半双工。

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

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

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

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

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


小结

以上,是 TCP 和 UDP 直观上的区别。
细节上海域很多很多的东西。
这个后面的博文中都会讲到。
【就是说:网络这一块,你们需要连接看,这几篇博客耦合性极强!】


UDP数据报 套接字编程

UDP socket 比 TCP 更简单。
我们先从简单的开始。


UDP Socket(套接字)编程中,主要涉及 两个类。

1、DatagramSocket(数据报套接字)
2、DatagramPacket(数据报 的数据包)

TCP 和 UDP 协议中,只有 UDP 是面向数据报的。
那么 DatagramScoket 和 DatagramPacket 这两类,从名字就能看出来(Datagram-数据报),是关于UDP协议的类。
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第5张图片


UDP协议 - DatagramSocket 的核心方法

方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacketp) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字

实战 : 写一个最简单的客户端服务器程序【回显服务】

什么是回显服务?
回显服务 - Echo Server

简单来说,我说什么,你回什么。
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第6张图片
就像回声,重复着我们说过话。

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


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


服务器部分

准备工作,在项目中创建一个 package 包。
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第7张图片
在所创建的包底下,创建两个类,分别是 UdpEchoServer(udp 回显服务器),UdpEchoClient(udp 回显客户端)
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第8张图片
回到,服务器类这一边,下面开始实现了。


构造一个 socket 对象

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第9张图片

需要注意的是:端口号可以是自己创建的,也可以是系统分配的。
当前这个写法,就属于自己分配的。
至于系统分配的,到后面也会跟你们讲的。


启动服务器

知识点:关于服务器 和 客户端的定义,在MySQL的 初步认识中 的 细谈MySQL 中讲了。
知识点:文件操作:输出型参数 在 文件内容相关的操作中讲到了。


回显服务器总程序

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//“五元组”
//站在服务器的角度:
//源IP:服务器程序所在主机的IP
//源端口:手动指定的端口【服务器绑定的端口】
//目的IP:包含在收到的数据报中【客户端的IP】
//目的端口:包含在收到的数据报中【客户端的端口】
//协议类型:UDP

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 datagramPacket= new DatagramPacket(new byte[1024],1024);
            socket.receive(datagramPacket);//为了接收数据,需要先准备好一个空的DatagramPacket对象,有receive来填充数据
            // 将 datagramPacket 解析成一个 String
            String request = new String(datagramPacket.getData(),0,datagramPacket.getLength(),"UTF-8");

            //2、根据请求计算响应(由于咱们这是一个回显服务,这一步就可以省略了)
            String response = process(request);
            //3、把响应写回客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    datagramPacket.getSocketAddress());
            socket.send(responsePacket);

            // 打印具体 IP、端口、请求、响应
            System.out.printf("[%s:%d] request: %s,response: %s\n",
                    datagramPacket.getAddress().toString(),// 客户端IP
                    datagramPacket.getPort(),// 客户端端口号
                    request,//请求
                    response);// 响应
        }
    }
    // 由于是回显服务,所以响应就和请求一样
    //但是实际上,对于一个真实的服务器来说,这个过程(根据请求计算响应),是最复杂的!
    // 为了实现这个过程,可能需要几万行,甚至几十万行代码。。。
    private static String process(String request){
        return  request;
    }
    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第10张图片


客户端实现

构造一个 socket 对象

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第11张图片


启动客户端

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第12张图片


回显客户端总程序

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;

//站在客户端的角度:
//源IP:本机IP
//源端口:系统分配的端口
//目的IP:服务器IP
//目的端口:服务器的端口
//协议类型:UDP
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
        //此处的 serverPort 是服务器的端口
        // 服务器启动的时候,不需要 socket来指定窗口,客户端自己的端口是系统随机分配的。
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    // 启动客户端
    public void start() throws IOException {
        Scanner sc = new Scanner(System.in);
        while(true){
            //1、先从控制台读取用户输入的字符串
            System.out.println("->");
            String request = sc.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 {
        //由于服务器 和 客户端 在同一个机器上,所以使用的 IP,仍然是 127.0.0.1,如果是在不同的机器上,这里IP就需要更改了。
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}


服务器 和 客户端的交互效果图

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第13张图片

这才是我们服务器正常的运行状态,同时处理多个客户端发送的请求。

拓展:
通常情况下,一个服务器,是要同时给多个客户端提供服务的。
但是也有特殊情况下,一个服务器只给一个客户端提供服务。

典型例子就是在分布式系统中,两个节点之间的交互。
一个节点视为服务器
另一个及诶单视为客户端
这种就属于是一个专属的情况,它不能给其他人随便乱请求。

给大家讲一个真实案例
清华大学知道吧?
娃哈哈知道吧?
娃哈哈老板的女儿当时就在清华大学读书。
为了让自己的女儿吃上家乡菜,专门就在清华门口开了个饭店。。。
这才是壕无人性的 一对一服务。
当然现在其他人也可以吃。


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

写一个翻译程序(英译汉)
请求是一些简单的英文单词。
响应是 英文单词 对应的 中文翻译。
客户端不变,把服务器代码进行调整。
主要是调整 process 方法。
其他步骤都是一样的。
关键的逻辑就是“根据想求来处理响应”
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第14张图片


字典服务器总程序

知识点:多态,继承

import network.UdpEchoServer;

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

// 创建一个类 UdpDictionaryServer 来表示  字典服务器
// 因为代码的逻辑几乎是一样,且所有的办法都是public的
// 所以我们在这里就直接继承,就可以使用内部所有的方法,并且可以进行重写操作。
public class UdpDictionaryServer extends UdpEchoServer {
    HashMap<String,String> map = new HashMap<>();//利用HashMap 来构建词库
    public UdpDictionaryServer(int port) throws SocketException {
        super(port);
        map.put("cat","小猫");
        map.put("dog","小狗");
        map.put("pig","佩奇");
    }

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

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


效果图

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第15张图片


总结

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

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

MySQL 也是 服务器-客户端,这样的结构。
输入/请求:SQL语句
输出/响应:可能是一个临时表,可能是 返回一个影响的行数、

再比如:打开一个网页,输入/请求 一个 奥特曼
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第16张图片


TCP流套接字编程

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


实战:回显服务 - TCP版本

服务器实现


构造一个 ServerSoceket 对象

这个和前面是TCP是一样,就不讲了。

package network;

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

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

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

启动服务器程序 - 与 UDP 差别,尽体现于此部分

知识点:文件操作
知识点:多线程中讲过进程是系统分配资源的单位,我忘记在那张图里讲过了。。
你们直接把 多线程的文章都看了吧。
网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第17张图片


TCP服务器总程序

package network;

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

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){
            //由于 TCP 是有连接的、因此,不能一上来就读取数据,需要先建立连接
            // accept 就是在“接电话”,接电话的前提是:有人给你打电话【有客户端发送请求】
            Socket clientSocket = listenSocket.accept();
            processConnection(clientSocket);// 处理连接成功的客户端请求
        }
    }

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

                    //2、根据请求,计算响应
                    String response = process(request);

                    //3、将响应返回给客户端
                    //为了方便起见,可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter =new PrintWriter(outputStream);
                    printWriter.println(response);
                    printWriter.flush();// 刷新缓冲区。
                    // 如果没有这个 flush,可能 客户端就不能第一时间看到响应的结果

                    System.out.printf("[%s:%d] request:%s,response:%s\n",clientSocket.getInetAddress(),// IP
                            clientSocket.getPort(),// 端口
                            request,//请求
                            response);// 响应
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }
    // 因为是回显服务,不涉及业务。只需要直接返货就可以了
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    } 
}

TCP客户端实现

构造一个 socket对象

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第18张图片


启动客户端程序

    public void start(){
        System.out.println("和进服务器连接成功!");
        Scanner sc = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream()){
            try (OutputStream outputStream = socket.getOutputStream()){
                while(true){
                    //1、从控制台读取字符串
                    System.out.println("->");
                    String request = sc.next();
                    //2、根据读取的自妇产,构造请求,把请求发送给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);// 看似是一个输出语句,其实已经将数据写到服务器里面去了
                    printWriter.flush();// 记得 立即刷新缓冲区,确保 服务器 第一时间 感知到 请求。
                    //3、从服务器读取响应,并解析
                    Scanner scanner = new Scanner(inputStream);
                    String response = scanner.next();
                    //4、把结果显示到控制台上。
                    System.out.printf("request:%s,response:%s\n ",request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

客户端总程序

package network;

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

public class TcpEchoClient {
    // 用普通的 Socket 即可,不用 ServerSocket 了
    private Socket socket = null;

    //此处也不用手动给客户端指定端口号,由系统自动分配(隐式)
    public TcpEchoClient(String serverIP,int serverPort) throws IOException {
        // 其实这里是可以给定端口号的,但是这里给了之后,含义是不同的。
        // 这里传入的 IP 与 端口号 的 含义: 表示的不是自己绑定,而是表示 和 这个IP 端口 建立连接
        socket = new Socket(serverIP,serverPort);// 这里表示 与 IP 为serverIP的主机上的 端口为9090的程序,建立连接。
    }
    public void start(){
        System.out.println("和进服务器连接成功!");
        Scanner sc = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream()){
            try (OutputStream outputStream = socket.getOutputStream()){
                while(true){
                    //1、从控制台读取字符串
                    System.out.println("->");
                    String request = sc.next();
                    //2、根据读取的自妇产,构造请求,把请求发送给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);// 看似是一个输出语句,其实已经将数据写到服务器里面去了
                    printWriter.flush();// 记得 立即刷新缓冲区,确保 服务器 第一时间 感知到 请求。
                    //3、从服务器读取响应,并解析
                    Scanner scanner = new Scanner(inputStream);
                    String response = scanner.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代码已将跑起来了还是此处还存在一个很严重的问题!!!!

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


TCP 服务器优化版 - 多线程版本

知识点:多线程基础篇

package network;

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

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){
            //由于 TCP 是有连接的、因此,不能一上来就读取数据,需要先建立连接
            // accept 就是在“接电话”,接电话的前提是:有人给你打电话【有客户端发送请求】
            Socket clientSocket = listenSocket.accept();
            Thread t = new Thread(()->{
                try {
                    processThreadConnection(clientSocket);// 处理连接成功的客户端请求
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }

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

                    //2、根据请求,计算响应
                    String response = process(request);

                    //3、将响应返回给客户端
                    //为了方便起见,可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter =new PrintWriter(outputStream);
                    printWriter.println(response);
                    printWriter.flush();// 刷新缓冲区。
                    // 如果没有这个 flush,可能 客户端就不能第一时间看到响应的结果

                    System.out.printf("[%s:%d] request:%s,response:%s\n",
                            clientSocket.getInetAddress(),// IP
                            clientSocket.getPort(),// 端口
                            request,//请求
                            response);// 响应
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }
    // 因为是回显服务,不涉及业务。只需要直接返货就可以了
    private String process(String request) {
        return request;
    }

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


效果图

网络编程(TCP 与 UDP协议) - JavaEE初阶 - 细节狂魔_第19张图片

此时我们才真正完成了 TCP 代码。


拓展1:TCP 服务器 - 线程池版本

知识点:多线程基础篇

package network;

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;

public class TcpThreadPool {
    private ServerSocket listenSocket = null;
    public TcpThreadPool(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService pool = Executors.newCachedThreadPool();
        while(true){
            Socket clientSocket = listenSocket.accept();
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream()){
            try (OutputStream outputStream = clientSocket.getOutputStream()){
                Scanner sc = new Scanner(inputStream);
                while (true){
                    if(!sc.hasNext()){
                        System.out.printf("[%s:%d] 客户端断开连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    String request = sc.next();
                    String response = process(request);
                    PrintWriter printWriter= new PrintWriter(outputStream);
                    printWriter.println(response);
                    printWriter.flush();

                    System.out.printf("[%s:%d] request:%s response:%s\n",
                            clientSocket.getInetAddress(),
                            clientSocket.getPort(),
                            request,
                            response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

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

拓展2:TCP 线程池版本服务器 - 带业务:字典

package network;

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

public class TcpEchoClient {
    // 用普通的 Socket 即可,不用 ServerSocket 了
    private Socket socket = null;

    //此处也不用手动给客户端指定端口号,由系统自动分配(隐式)
    public TcpEchoClient(String serverIP,int serverPort) throws IOException {
        // 其实这里是可以给定端口号的,但是这里给了之后,含义是不同的。
        // 这里传入的 IP 与 端口号 的 含义: 表示的不是自己绑定,而是表示 和 这个IP 端口 建立连接
        socket = new Socket(serverIP,serverPort);// 这里表示 与 IP 为serverIP的主机上的 端口为9090的程序,建立连接。
    }
    public void start(){
        System.out.println("和进服务器连接成功!");
        Scanner sc = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream()){
            try (OutputStream outputStream = socket.getOutputStream()){
                while(true){
                    //1、从控制台读取字符串
                    System.out.println("->");
                    String request = sc.next();
                    //2、根据读取的自妇产,构造请求,把请求发送给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);// 看似是一个输出语句,其实已经将数据写到服务器里面去了
                    printWriter.flush();// 记得 立即刷新缓冲区,确保 服务器 第一时间 感知到 请求。
                    //3、从服务器读取响应,并解析
                    Scanner scanner = new Scanner(inputStream);
                    String response = scanner.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();
    }
}


拓展3:细节问题

一个 TCP服务器,能否让一个 UDP 客户端连上?
答案:不行的!

TCP 和 UDP,它们无论是 API 代码,还是协议底层的工作过程,都是差异巨大的。
不是单纯的 “把流转换成数据报”就可以的,这两者之间毫无关系。

我们需要明白:
一次通信,需要用到五元组:源IP,源端口,目的IP,目的端口,协议类型。
如果协议类型不匹配,那么两者之间是无法通信的。


通信双方必须使用同一种协议,才能顺利的进行通信。

你可能感兴趣的:(JavaEE初阶,java-ee,网络编程)