javaEE初阶---网络原理(套接字)

一 : 网络发展史

1.1独立模式

核战争,简称“核战”,总括来说是属于使用核武器的战争 . 在核武器历史上,曾被使用的战争中只有在第二次世界大战中的广岛市原子弹爆炸、长崎市原子弹爆炸使用 . 在二十世纪六十年代 , 美苏争霸 ,古巴导弹危机,是1962年发生的事件,是冷战期间美国、苏联两国之间最激烈的一次对抗 . 而发射核弹 , 有三个重要部分 :

javaEE初阶---网络原理(套接字)_第1张图片
所以当时米国人就研究 , 能不能搞一个网络通信 , 不怕核弹的打击 , 即让整个通信链路变得复杂 , A->B之间有多个通信链路 !即使其中某些链路受到打击 , 剩下的链路仍能完成通信 .

这就演化成了互联网 , 虽然其没有起到预期的效果 , 但是基于其在传输数据方面发挥的重大作用 , 越来越多的计算机接入互联网 , 就构成了今天最大的互联网 -------Internet(因特网) . 而internet泛指互联网 ,即广义上的网络 .

1.2网络互连

随着时代的发展,越来越需要计算机之间互相通信,共享软件和数据,即以多个计算机协同工作来完成业务,就有了网络互连。
网络互连:将多台计算机连接在一起,完成数据共享。
数据共享本质是网络数据传输,即计算机之间通过网络来传输数据,也称为网络通信。

根据网络互连的规模不同,可以划分为局域网和广域网。

1.局域网
局域网,即 Local Area Network,简称LAN。
把多台机器连接到一起 , 就构成了局域网 .
局域网组建网络的方式有多种 :

  1. 基于网线组建 ;
    javaEE初阶---网络原理(套接字)_第2张图片

两两相连的话 , 没那么多网线/网口 , 成本太高 ,所以就诞生了集线器 , 交换机和路由器 .

  1. 基于集线器组建 ;

javaEE初阶---网络原理(套接字)_第3张图片
javaEE初阶---网络原理(套接字)_第4张图片

  1. 基于交换机组建 ;
    4.javaEE初阶---网络原理(套接字)_第5张图片 3. 基于交换机组建;javaEE初阶---网络原理(套接字)_第6张图片
    javaEE初阶---网络原理(套接字)_第7张图片

     交换机,是把多个机器连接到一个局域网中 ;
    
  2. 基于交换机和路由器组建 ;

javaEE初阶---网络原理(套接字)_第8张图片

路由器:连接多个局域网;

2.广域网WAN

广域网,即 Wide Area Network,简称WAN .
在交换机和路由器的反复连接之下 , 就可以把很多很多设备都连接到一起 , 使它们可以进行通信 .
当机器足够多时 , 就可以成为"广域网" .

局域网和广域网 , 没有明确的界限 ! 广域网内部的局域网 , 都属于其子网 .

二:网络中的重要概念

2.1协议

定义 : 网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。

协议其实就是"约定" , 通过一些约定 , 表达一定的含义 .

网络上传输的数据,本质上是光信号/电信号

  • 光信号,频率,例如,高频1,低频0
  • 电信号,电平,例如,高电平1,低电平0

不同的01排列组合,都要表达什么样的信息呢?都需要通过"协议"来约定!!!

网络通信非常复杂 , 如果使用一个协议来约定所有的问题 , 势必非常复杂 ; 所以大佬们就进行了程序拆分 , 把一个协议拆分成多个协议 . 拆着拆着 , 又发现许多协议其实解决了类似的问题 , 就把这些协议进行了分层 , 每一层内的诸多协议解决的问题都是类似的 .

Q : 为什么需要网络协议的分层 ?

A :

  1. 分层最大的好处,类似于面向接口编程:定义好两层间的接口规范,让双方遵循这个规范来对接。下层协议给上层提供服务 , 上层调用下层协议 .(封装)
  • 对于使用方来说,并不关心提供方是如何实现的,只需要使用接口即可 ;
  • 对于提供方来说,利用封装的特性,隐藏了实现的细节,只需要开放接口即可 .
  1. 解耦合 , 可以把同一层的协议替换成其他协议 , 对于其他层次的协议来说 , 基本是无感知的(透明的)…

javaEE初阶---网络原理(套接字)_第9张图片

2.2分层模型

当前有两种分层模型.

参考资料 : 分层模型简介

2.2.1 OSI七层网络模型

物链网传会表用.

OSI:即Open System Interconnection,开放系统互连 .

OSI七层模型复杂且不实用 , 所以只是理论上存在 .

javaEE初阶---网络原理(套接字)_第10张图片

2.2.2 TCP/IP五层网络模型

2.2.2.1概述

实际上实现的 , 是TCP/IP五层网络模型 .

javaEE初阶---网络原理(套接字)_第11张图片
javaEE初阶---网络原理(套接字)_第12张图片
只有应用层是程序猿自己写代码进行处理 , 其余各层都是由操作系统和硬件设备处理好的 .

网络编程的主要工作 , 就是写应用层的代码 , 来处理应用层的协议数据 .

理解网络层和数据链路层

比如我上学 , 要从沁源县到西安市 . 可以有这些路径 :

  1. 沁源—>长治—>太原—>西安 ;
  2. 沁源—>临汾—>西安;
  3. 沁源—>阳泉—>咸阳—>西安;

考虑走哪条路线 , 是网络层的工作 . (宏观) ,比如走第2条路线 .

考虑相邻节点之间具体怎么走 , 是数据链路层的工作. (微观) , 比如沁源—>临汾坐大巴车 , 临汾—>西安坐高铁 .

2.2.2.2封装和分用

2.2.2.2.1基本概念
  • 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据(datagram),在链路层叫做帧(frame) .
  • 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation) .
  • 首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息 .
  • 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中
    的 “上层协议字段” 将数据交给对应的上层协议处理 .
2.2.2.2.2封装
栗子:主机 A 通过微信发送 "hello" 给主机 B.

javaEE初阶---网络原理(套接字)_第13张图片

javaEE初阶---网络原理(套接字)_第14张图片
javaEE初阶---网络原理(套接字)_第15张图片
javaEE初阶---网络原理(套接字)_第16张图片

javaEE初阶---网络原理(套接字)_第17张图片
上述从上到下 , 依次添加报头的过程 , 称为"封装" .

javaEE初阶---网络原理(套接字)_第18张图片

2.2.2.2.3分用

省略中间的传输过程 . 当主机B收到上述数据时 , 就是封装的逆过程 , 称为"分用" , 把每一层协议对应的报头给解析出来 , 并且去掉报头 . (拆快递) .

javaEE初阶---网络原理(套接字)_第19张图片
javaEE初阶---网络原理(套接字)_第20张图片

上述过程就体现出 , 网络通信中 , 各个层次的协议是如何配合工作的 .

上述的封装分用不仅出现在主机上 , 同样出现在传输过程中 ,包括在交换机和路由器上 .

  • 经典路由器 , 是封装分用到网络层 ;
  • 经典交换机 , 是封装分用到数据链路层 .

三 : 网络编程

3.1 为什么需要网络编程 ?

用户在浏览器中,打开在线视频网站,如在B站观看视频,实质是通过网络,获取到网络上的一个视频资源 . 除了视频资源 , 网络上还有很多资源 , 如图片资源 , 文件资源 , 音乐资源…

javaEE初阶---网络原理(套接字)_第21张图片

所谓的网络资源,其实就是在网络中可以获取的各种数据资源。

而所有的网络资源,都是通过网络编程来进行数据传输的。

3.2网络编程中的重要概念

3.2.1网络编程

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

通过写代码 , 实现两个/多个进程之间 , 通过网络进行相互通信 …

我们知道 , 进程具有隔离性 , 每个进程都有自己的独立虚拟地址空间 , 进程间通信 , 就是借助每个进程都能访问到的公共区域 , 实现数据的交换 . 网络编程借助的公共区域就是"网卡" . 通过这种方式 , 既可以让同一个主机的多个进程之间通信 , 也可以让不同主机的多个进程之间通信 .

网卡 :网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件。由于其拥有MAC地址,因此属于OSI模型的第1层和2层之间。它使得用户可以通过电缆或无线相互连接。
每一个网卡都有一个被称为MAC地址的独一无二的48位串行号,它被写在卡上的一块ROM中。在网络上的每一个计算机都必须拥有一个独一无二的MAC地址。
没有任何两块被生产出来的网卡拥有同样的地址。这是因为电气电子工程师协会(IEEE)负责为网络接口控制器(网卡)销售商分配唯一的MAC地址。

3.2.2发送端和接收端

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

注意 : 发送端和接收端是相对的 , 只是一次数据传输过程中 , 发数据的那端就叫发送端了 .

javaEE初阶---网络原理(套接字)_第22张图片

3.2.3客户端(client)/服务器(server)

  • 客户端 : 主动发送网络数据的一方
  • 服务器 : 被动接受网络数据的一方

服务器无法知道客户端什么时候来 , 所以服务器会长时间运行, 甚至7*24运行 .

javaEE初阶---网络原理(套接字)_第23张图片

3.2.3请求(request)/响应(response)

  • 请求 : 客户端给服务器发送的数据 .
  • 响应 : 服务器给客户端返回的数据 .

javaEE初阶---网络原理(套接字)_第24张图片

3.2.4 客户端和服务器之间的交互方式

  1. 一问一答 : 客户端给服务器发一个请求 , 服务器就给客户端返回一个响应 . eg:浏览网页
  2. 多问一答 : 客户端发多个请求 , 服务器返回一个响应 . eg:发送文件
  3. 一问多答 : 客户端发一个请求 , 服务器返回多个响应 . eg:下载文件
  4. 多问多答 : 客户端发多个请求 , 服务器返回多个响应 . eg:远程控制

3.3Socket套接字

3.3.1概念

进行网络编程 , 需要使用操作系统提供的网络编程API .

应用层要想进行网络编程的相关操作 , 就要调用传输层提供的一些功能 . 传输层就提供了网络通信的API , 这些API叫Socket API .

javaEE初阶---网络原理(套接字)_第25张图片
传输层提供了两个非常重要的协议 , 而且截然不同 . 他们就是TCPUDP. 这两个协议对应的Socket api 也是截然不同的 .

TCP和UDP的特点

javaEE初阶---网络原理(套接字)_第26张图片

3.3.2 关于TCP面向字节流的解读

面向字节流:

创建一个TCP的socket,会在网络中同时创建一个发送缓冲区和接收缓冲区。

刚开始会将数据写入发送缓冲区。若数据太短,则在发送缓冲区中等待,等到合适时机会将合适大小的数据以字节流的形式发送出去;若数据太长,则进行拆分,然后发送。

由于TCP是全双工的,所以读写数据时没有限制,可以一次性接收所有数据;也可以每次接收一点,分多次接收。

TCP面向字节流的特点是:传输灵活,但是存在粘包问题。

Q : 什么是粘包问题 ?

A :
发送端为了将多个发往接收端的包 , 更有效的发给对方 , 使用了优化方法(Nagle算法),将多次间隔较小 , 数据量较小的数据包,合并成一个大的数据包发送(把发送端的缓冲区填满一次性发送)。从接收缓冲区看 , 后一个包的头连接着其前一个包的尾 , 好像粘在了一起 , 这就是"粘包" .

Q : 造成TCP粘包的原因 ?

A :
(1)发送方原因

TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:

  1. 只有上一个分组得到确认,才会发送下一个分组
  2. 收集多个小分组,在一个确认到来时一起发送

Nagle算法造成了发送方可能会出现粘包问题 .

(2)接收方原因

TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。

实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。

Q : 什么是丢包问题 ?

A :
udp是放在数据帧里面传输的 , 数据帧最多放1500个字节,除去udp的报头 一个数据帧最多发送1472个字节的udp , 如果udp过大 , 就会被拆分成多个数据帧发送到网络 , 即会造成丢包的现象。

Q : 为什么UDP不会粘包 ?

A :
TCP协议是面向流的协议,UDP是面向消息的协议 , 系统不会缓冲也不会优化 . UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据 .

其次 , TCP只要保证自己写入的流是按 长度 + 内容 + 长度 + 内容 , 这样就可以非常简单的解决粘包问题 .

参考资料 : TCP粘包与UDP丢包的原因

3.4UDP的Socket API

3.4.1 UDP Socket API中的核心类

3.4.1.1综述

javaEE初阶---网络原理(套接字)_第27张图片

3.4.1.2DatagramSocket

DatagramSocket的构造方法
javaEE初阶---网络原理(套接字)_第28张图片

DatagramSocket的方法

javaEE初阶---网络原理(套接字)_第29张图片

3.4.1.3DatagramPacket

DatagramPacket的构造方法

javaEE初阶---网络原理(套接字)_第30张图片

DatagramPacket的方法

javaEE初阶---网络原理(套接字)_第31张图片

3.4.1.4InetSocketAddress

3.4.2具体实例

写一个UDP版本的回显服务器-客户端.

注意 : 客户端发啥 , 服务器就返回啥, 不涉及任何业务逻辑 , 仅单纯演示API的用法 .

3.4.2.1 服务器代码

import javax.sql.DataSource;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    //要想创建UDP服务器,首先要打开一个socket文件
    private DatagramSocket socket = null;

    //提供构造方法,为该进程分配一个端口号
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器通常需要长时间工作,所以使用while(true)
        while(true){
            //1.读取客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //2.对请求进行解析,把DatagramPacket转成一个String
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //3.根据请求,处理响应.
            String response = process(request);
            //4.把响应构造成DatagramPacket对象.
            //构造响应对象,要搞清楚,对象要发给谁!谁发送的请求,就把响应发给谁.
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            //5.把这个DatagramPacket对象返回给客户端
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req=%s ;resp=%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),
                    request,response);
        }
    }
    // 通过这个方法, 实现根据请求计算响应 这个过程.
    // 由于是回显服务器, 所以不涉及到其他逻辑.
    // 但是如果是其他服务器, 就可以在 process 里面, 来加上一些其他逻辑的处理.
    public String process(String request) {
        return request;
    }

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

3.4.2.2 程序分析

javaEE初阶---网络原理(套接字)_第32张图片
javaEE初阶---网络原理(套接字)_第33张图片
javaEE初阶---网络原理(套接字)_第34张图片

在这里插入图片描述
javaEE初阶---网络原理(套接字)_第35张图片

3.4.2.3 客户端代码

import java.awt.dnd.DropTarget;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;

    //客户端端口号,一般由操作系统自动分配
    public UdpEchoClient() throws SocketException {
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while(true) {
            //1.客户端从控制台读取一个请求数据
            System.out.println("> ");
            String request = scanner.next();
            //2.字符串发送给服务器,构造DatagramPacket
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName("127.0.0.1"), 8000);
            //3.把数据报发给服务器
            socket.send(requestPacket);
            //4.从服务器读取响应数据
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //5.获取响应数据,转成字符串
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());

            System.out.printf("req:%s ; resp:%s\n", request, response);
        }
    }

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

3.4.2.4 程序分析

javaEE初阶---网络原理(套接字)_第36张图片
javaEE初阶---网络原理(套接字)_第37张图片
代码中使用到的三种构造DatagramPacket的方法总结 :
javaEE初阶---网络原理(套接字)_第38张图片
其中1 , .用于接收数据 , 其余方法均用于发送数据 .

3.4.2.5 客户端和服务器的工作流程

javaEE初阶---网络原理(套接字)_第39张图片
javaEE初阶---网络原理(套接字)_第40张图片
运行效果 :

一个服务器 , 是可以给多个客户端提供服务的 . 需要对IDEA进行设置 .
javaEE初阶---网络原理(套接字)_第41张图片
javaEE初阶---网络原理(套接字)_第42张图片
同时启动两个客户端 :

javaEE初阶---网络原理(套接字)_第43张图片

javaEE初阶---网络原理(套接字)_第44张图片

javaEE初阶---网络原理(套接字)_第45张图片
javaEE初阶---网络原理(套接字)_第46张图片
如果我现在需要写一个带有业务逻辑的服务器 , 咋搞 ?

这个容易处理 , 直接继承UdpEchoServer服务器 , 重写其中的process()方法即可 ! 这就是说 , process()方法不能设计成private类型的 .

例如实现一个具有英译汉功能的服务器 .

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

public class UdpDictServer extends UdpEchoServer {
    private Map<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);


    dict.put("cat","猫");
    dict.put("apple","苹果");
    dict.put("dog","狗");
}

        public String process(String req){
            return dict.getOrDefault(req,"别来沾边儿");
        }

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

运行结果 :
javaEE初阶---网络原理(套接字)_第47张图片
javaEE初阶---网络原理(套接字)_第48张图片
小技巧

当我们发现某个端口,被其他进程占用了,导致咱们的服务器起不来.

通过netstat命令来找到是那个进程占用的.

netstat -ano | findstr “8000” 就能找到对应的pid(端口号)了.

在这里插入图片描述

3.5TCP的Socket API

3.5.1 TCP Soxcket API中的核心类

3.5.1.1综述

javaEE初阶---网络原理(套接字)_第49张图片

注意 : 该综述非常非常重要!!!
javaEE初阶---网络原理(套接字)_第50张图片

3.5.1.2ServerSocket

ServerSocket的构造方法

在这里插入图片描述

ServerSocket的方法

javaEE初阶---网络原理(套接字)_第51张图片

3.5.1.3Socket

Socket的构造方法

javaEE初阶---网络原理(套接字)_第52张图片

Socket的方法
javaEE初阶---网络原理(套接字)_第53张图片

3.5.2 具体实例

3.5.2.1 服务器代码

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 TcpEchoServer {

    private ServerSocket serverSocket = null;

    //提供构造方法,手动指定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true) {
            //.如果当前没有客户端来建立连接,accept会阻塞等待
            Socket clientSocket = serverSocket.accept();

            //1.单线程版本,只能处理单个客户端
            //processConnect(clientSocket);

            //2.多线程版本,主线程负责拉客,新线程负责通信
            //.较版本一有提升,但是涉及到频繁创建销毁线程,在高并发的情况下,负担比较重.
//            Thread t = new Thread(()->{
//                try {
//                    processConnect(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();

            //3.线程池版本,解决了频繁创建销毁线程的问题
            service.submit(new Runnable(){

                @Override
                public void run() {
                    try {
                        processConnect(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    /**
     * 通过这个方法,给当前的这个客户端,提供服务!
     * 一个连接过来了,服务方式可能有两种:
     * 1.一个连接只进行一次数据交互(一个请求,一个响应) 短连接
     * 2.一个连接只进行多次数据交互(N个请求,N个响应) 长连接
     * //此处是长连接版本
     * @param clientSocket
     */
    public void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 建立连接!\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            //长连接,通过循环来实现多次交互!
            while(true) {
                if(!scanner.hasNext()){
                    //.连接断开,当客户端连接断开,hasNext会返回false
                    System.out.printf("[%s:%d]断开连接!\n",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                //1.读取请求并解析
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回给客户端
                printWriter.println(response);
                //4.刷新缓冲区
                printWriter.flush();
                System.out.printf("[%s:%d] req:%s;resp:%s\n ",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }

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

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


}

3.5.2.2 程序分析

javaEE初阶---网络原理(套接字)_第54张图片

上述分析针对单线程版本 , 只能处理一个客户端 . 为什么呢 ?

javaEE初阶---网络原理(套接字)_第55张图片

javaEE初阶---网络原理(套接字)_第56张图片
javaEE初阶---网络原理(套接字)_第57张图片

不要误以为TCP服务器必须使用多线程处理 !

TCP服务器这里之所以使用了多线程,是因为在代码中处理了"长连接".客户端建立好连接后,不确定什么时候断开连接,也不确定一个连接中要处理的请求数量(因为是长连接),所以就导致在处理连接的代码处循环,主循环就无法执行到accept了.如果代码仅用于处理短连接,即每次客户端一个连接只处理一个请求,不使用多线程也能处理多个客户端.

上述版本二中 , 可以同时处理多个服务器了 , 但是涉及到频繁创建销毁线程 , 在高并发的情况下 , 负担比较重 .

解决方案 : 版本三,线程池!

javaEE初阶---网络原理(套接字)_第58张图片

拓展 :

线程池可以解决频繁创建销毁线程的问题 . 但如果并法量太高 , 就会导致池子里的线程特别多 ,造成内存等资源的过度开销 .

进一步考虑 , 能否减少线程的数目呢 ? 当前是一个线程对应一个客户端 , 能否让一个线程对应多个客户端呢 ?

这就是IO多路复用 , 本质上是一个线程处理多个socket . 在java封装后 , 是NIO .

关于NIO , 可以参考下文 , 此处不进行展开 .

什么是NIO ? NIO的原理是什么 ?

3.5.2.3 客户端代码

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

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient() throws IOException {
        socket = new Socket("127.0.0.1",8000);
    }

    public void start(){
        //长连接,一个连接会处理N个请求和响应
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while(true){
                //1.从控制台读取用户的输入
                System.out.println("> ");
                String request = scanner.next();
                //2.把请求发送给服务器
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应
                String response = scannerNet.next();
                //4.把结果显示到界面上
                System.out.printf("req:%s;resp=%s\n",request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient();
        client.start();
    }
}

3.5.2.4程序分析

javaEE初阶---网络原理(套接字)_第59张图片
javaEE初阶---网络原理(套接字)_第60张图片

Q : 在服务器的代码中 , 我们使用了hasNext()判断是否有响应中断 , 而在客户端代码中则没有这么做 .Why ?

A :

  • 这是因为我们默认在客户端程序中发出的请求 , 是一定会响应的 ; 服务器会响应我的请求吗 ? 必须的必啊!
  • 而在服务器端 , 处理完一次交互后 , 不确定是否还有下一次交互 . 客户端发送一个请求之后 , 还会发下一个请求吗 ? 这是不确定的 .

Q : 客户端只是用一个String接收响应 , 如果是一次请求 , 多次响应 ,怎么办 ?

A :

TCP的本质 : 字节流

如果服务器返回多次响应 , 这多次响应 , 都是以字节为单位返回给客户端的 .

客户端操作系统内核就会受到这些字节 , 之后调用read/scanner.next()这样的方法从内核中读取数据 .

例如使用read时 ,返回10次 , 每次返回10个字节 , 只需read(buf[100])就可以一次都读取出来了 …

而scanner.next()则是读到空白符返回 , 即空格,回车,换行,制表,翻页… ,返回值中不包含空白符!(无论最后有几个空白符 , 都不会被包含在返回值内!)

也就是说 , 不管服务器怎么返回 , 客户端这边是爱怎么取怎么取 ,一次取完也行 , 分多次取完也行 .

Q : 我在客户端代码中, 发送请求时使用了write而不是println ,; 在服务器代码中 , 返回响应也使用了write而不是println , 代码跑不起来 , 效果如下图 , 为什么 ?

javaEE初阶---网络原理(套接字)_第61张图片

javaEE初阶---网络原理(套接字)_第62张图片
分析 :

输入内容 , 发现没响应 , 可能是 :

  1. 客户端没发送出去 .
  2. 服务器收到了 , 但没有处理 .

此时服务器还是在阻塞的状态 , 有可能是没有加flush(),但是我们的代码中是有的 .

如何确认阻塞在哪里呢 ? 可以使用jconsole查看当前进程的情况 , 可以看到每个进程的调用栈 .

javaEE初阶---网络原理(套接字)_第63张图片
javaEE初阶---网络原理(套接字)_第64张图片
javaEE初阶---网络原理(套接字)_第65张图片
在这里插入图片描述
分析过程 :

javaEE初阶---网络原理(套接字)_第66张图片

通过TCP 的Socket API , 写一个翻译功能的服务器 .(仍使用原来的客户端代码 , 改变服务器代码即可)

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

public class TcpDictServer extends TcpEchoServer {

    private Map<String,String> dict = new HashMap<>();

    @Override
    public String process(String request) {
        return dict.getOrDefault(request,"别来沾边儿");
    }

    public TcpDictServer(int port) throws IOException {
        super(port);

        dict.put("cat","猫");
        dict.put("dog","狗");
        dict.put("apple","苹果");
    }

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

}

javaEE初阶---网络原理(套接字)_第67张图片

本课内容结束 !

你可能感兴趣的:(javaEE初阶,网络,服务器,运维)