# java网络编程 **Repository Path**: sealedgodjn/java-network-programming ## Basic Information - **Project Name**: java网络编程 - **Description**: 2021-10-15~2021-10-17 完成TCP、UDP的复习,深入学习BIO和其他网络框架模型 - **Primary Language**: Java - **License**: AFL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-10-16 - **Last Updated**: 2021-10-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Socket ## 学习目标与收获 ![image-20211014202419507](readme.assets/image-20211014202419507.png) ![image-20211014202340837](readme.assets/image-20211014202340837.png) ### 导学 ![image-20211014202637300](readme.assets/image-20211014202637300.png) ![image-20211014202644332](readme.assets/image-20211014202644332.png) ![image-20211014202709603](readme.assets/image-20211014202709603.png) ![image-20211014202730671](readme.assets/image-20211014202730671.png) ![image-20211014202752721](readme.assets/image-20211014202752721.png) ### 技术原理 ![image-20211014202826618](readme.assets/image-20211014202826618.png) ![image-20211014202907374](readme.assets/image-20211014202907374.png) ## 第2章 ![image-20211014203450441](readme.assets/image-20211014203450441.png) ![image-20211014203628610](readme.assets/image-20211014203628610.png) ![image-20211014203838866](readme.assets/image-20211014203838866.png) ### What is Socket? ![image-20211014204745793](readme.assets/image-20211014204745793.png) ### TCP ![image-20211014205404607](readme.assets/image-20211014205404607.png) ### UDP ![image-20211014205539288](readme.assets/image-20211014205539288.png) ### TCP传输 1、要么成功 2、要么失败 3、成功,则回复ack ### UDP传输 1、只管发送,不管回复 ### CS模型 ![image-20211014210138335](readme.assets/image-20211014210138335.png) ### 实现网络TCP服务器端和客户端——流程 ``` 服务器端 1、新建ServerSocket,监听端口,等待连接 2、使用一个内部静态类ClientHandler,继承Thread,可以异步连接client 3、每新来一个客户端socket,则新建一个ClientHandler对象 4、在ClientHandler中,继承Thread,实现run方法,在run方法中,完成打印操作 5、最后要关闭所有连接 客户端 1、新建Socket,设置ip和端口号,connect服务器端 2、todo方法,向服务器发送消息,输出回送的消息 3、最后释放所有资源 ``` #### 可改进 ```java Client 1、加了一个线程处理客户端的消息读取与打印作用在于读取消息时不阻塞数据的写入,让客户端可以一直根据用户的输入状况发送数据,不会因为读取网络数据而阻塞用户的输入导致假死的现象。 Thread thread = new Thread(() -> { int size = -1; byte[] bytes = new byte[1024]; StringBuilder sb = new StringBuilder(1024); try { while ((size = socket.getInputStream().read(bytes, 0, bytes.length)) > 0) { String message = new String(bytes, 0, size, "UTF-8"); sb.append(message); if (message.lastIndexOf("\n") > 0) { System.out.println(sb.toString()); sb.delete(0, sb.length()); } if (Thread.currentThread().isInterrupted()) { break; } } } catch (IOException e) { e.printStackTrace(); } }); thread.start(); ``` #### BIO缺点 ``` 熟话说没有对比就没有伤害,优点与缺点的列举是相对NIO而言; 先说下BIO的优点: 1、编程简单,模型便于理解, 2、适用单个大数据对象的高效传输; 相反BIO缺点 1、对高并发场景下对于线程资源的消耗较高,每一个连接需要使用一条线程单独处理, 2、传输较小对象时存在频繁的线程上下文切换等性能问题。 虽然BIO模型存在性能上的问题,但在如今NIO唱红时期,使用BIO模型通信的服务还是不在少数,主要原因一个是历史遗留问题,第二个是BIO也有它的优点,术业有专攻啊,像文件传输等需要传输大量数据的场景下还是比较适用BIO模型。 ``` #### BIO优化 ``` BIO 模型是阻塞的,模型本身就决定了在处理多连接的场景时不能使用单线程的方式去处理;有多少连接就需要多少线程,但对于用户来说打开一个连接,再关闭这个连接是常有的事,而这相对应的就是线程的创建与销毁;线程的创建与销毁对于系统资源来说是昂贵的,所以我们可以使用线程池来进行优化。具体的示例就不贴了,篇幅有限。 最后唠嗑说说为什么会有NIO的面世,这也和BIO 模型的优化有关;BIO模型是阻塞的,阻塞就导致不能使用单线程处理多个请求,但如果将BIO 模型修改,调用read() write()方法不再是阻塞的,那这样就可以使用单线程处理多个请求了;而这样的优化正是NIO的精髓所在。 ``` ### 报文段 ![image-20211016090130880](readme.assets/image-20211016090130880.png) ![image-20211016090226865](readme.assets/image-20211016090226865.png) ### 传输协议 ![image-20211016090253663](readme.assets/image-20211016090253663.png) ### Mac地址 ![image-20211016090830708](readme.assets/image-20211016090830708.png) ### IP地址 ![image-20211016091009871](readme.assets/image-20211016091009871.png) ![image-20211016091055564](readme.assets/image-20211016091055564.png) ![image-20211016091125670](readme.assets/image-20211016091125670.png) ### IPv6 ![image-20211016091209093](readme.assets/image-20211016091209093.png) ![image-20211016091222063](readme.assets/image-20211016091222063.png) ### 端口 ![image-20211016091318730](readme.assets/image-20211016091318730.png) ![image-20211016091335517](readme.assets/image-20211016091335517.png) ![image-20211016091449720](readme.assets/image-20211016091449720.png) ### 数据传输层次 ![image-20211016091720148](readme.assets/image-20211016091720148.png) ### 远程服务器 ![image-20211016092331461](readme.assets/image-20211016092331461.png) ### 连接示意图 ![image-20211016092421336](readme.assets/image-20211016092421336.png) ![image-20211016092507140](readme.assets/image-20211016092507140.png) 通过服务器交换数据 ### Web请求流程 ![image-20211016092540107](readme.assets/image-20211016092540107.png) ## 第3章 ### UDP ![image-20211016093314278](readme.assets/image-20211016093314278.png) ### 为什么不可靠 ![image-20211016094713795](readme.assets/image-20211016094713795.png) ### UDP报文格式 ![image-20211016094906465](readme.assets/image-20211016094906465.png) ### UDP包最大长度 ![image-20211016095259458](readme.assets/image-20211016095259458.png) ### API-DatagramSocket ![image-20211016095546633](readme.assets/image-20211016095546633.png) 监听固定端口,该端口用于接收数据 ![image-20211016095637447](readme.assets/image-20211016095637447.png) ### API-DatagramPacket ![image-20211016095736419](readme.assets/image-20211016095736419.png) ![image-20211016095804985](readme.assets/image-20211016095804985.png) ![image-20211016100857070](readme.assets/image-20211016100857070.png) ![image-20211016101212320](readme.assets/image-20211016101212320.png) ![image-20211016101234308](readme.assets/image-20211016101234308.png) 指定的地址和端口号都是目的地址和目的端口号 ### 实现UDPProvider和UDPSearcher ``` Provider 1、新建DatagramSocket,指定端口 2、构建接收实体buf(byte数组),使用DatagramPacket接收(receive)数据包 3、根据接收到的数据包,得到ip和Port,还有数据长度 4、使用DatagramPacket构建报文,通过datagramSocket(send)向Searcher回送报文 5、释放所有资源 Searcher 1、新建一个DatagramSocket,不用指定端口(作为搜索方,由系统随机分配端口) 2、发送一个request数据,转为byte数组,使用DatagramPacket构建报文,设置目的IP和目的Port,调用DatagraSocket发送requestPakcet 3、使用DatagramSocket接收服务器回送的报文,根据报文获得ip和port,还有数据报文的长度 ``` ### UDP单播、广播、多播(组播) ![image-20211016101947730](readme.assets/image-20211016101947730.png) ### IP地址类别 ![image-20211016102318808](readme.assets/image-20211016102318808.png) ### 广播地址 ![image-20211016102817985](readme.assets/image-20211016102817985.png) ### IP地址构成 ![image-20211016103054192](readme.assets/image-20211016103054192.png) 用一个int即可存储IP地址 ### 广播地址运算 ![image-20211016103244143](readme.assets/image-20211016103244143.png) ![image-20211016103303176](readme.assets/image-20211016103303176.png) ![image-20211016103431052](readme.assets/image-20211016103431052.png) ### 广播通信问题 ![image-20211016103942921](readme.assets/image-20211016103942921.png) ### 案例实操-局域网搜索案例 ![image-20211016112516799](readme.assets/image-20211016112516799.png) #### 通信过程分析 ``` 1、A从1000端口给B的2000端口发送UDP报文 2、B得到报文之后,知道了A的地址和端口号 3、根据上一步得到的地址和端口号,回送数据 ``` #### Provider和Searcher流程 ``` UDPProvider 1、生成唯一标识sn 2、启动线程provider 3、在线程provider中,新建DatagramSocket和DatagramPacket,调用receive函数,等待接收broadcast这个Packet 4、当接收到broadcast后,解析Packet,得到发送端的ip和port 5、新建DatagramPacket,设置目的IP和port,调用DatagramSocket发送回送报文 6、输入任意字符,线程释放所有资源 UDPSearcher 1、启用监听listener线程,在listener中,新建DatagramSocket(指定端口),等待Provider回送的报文 2、在主线程中,新建新建DatagramSocket,构建request数据,设置Packet的广播地址为“255.255.255.255”和端口号,调用DatagramSocket发送requestPacket 3、在Provider接收到request之后,Provider回送报文,listener接收到所有的回送报文,解析回送报文,如果sn不为null,则添加新的设备 4、输入任意字符,输出所有设备 5、关闭所有线程和资源 ``` #### 抓包分析 ![image-20211018200819765](readme.assets/image-20211018200819765.png) ![image-20211018200827750](readme.assets/image-20211018200827750.png) ``` 1、从UDPSearcher的63610到20000 2、从UDPSearcher的57082到20000 3、只发送一个数据包 ``` ## 第4章 ### TCP是什么 ![image-20211017152021368](readme.assets/image-20211017152021368.png) ![image-20211017152202361](readme.assets/image-20211017152202361.png) ### TCP连接、传输流程 ![image-20211017152227633](readme.assets/image-20211017152227633.png) ### TCP能做什么 ![image-20211017152735255](readme.assets/image-20211017152735255.png) ### TCP核心API ![image-20211017152846478](readme.assets/image-20211017152846478.png) ![image-20211017152943410](readme.assets/image-20211017152943410.png) ### 流程分析 ![image-20211017153128428](readme.assets/image-20211017153128428.png) ![image-20211017153151093](readme.assets/image-20211017153151093.png) ### Socket与进程的关系 ![image-20211017153428217](readme.assets/image-20211017153428217.png) 1、每一个进程都可以创建一个Socket连接 2、程序只能操控进程?不能操控TCP处理buffers and variables? TCP强连接、代理、优化 ### TCP连接可靠性 #### 三次握手 ![image-20211017154311447](readme.assets/image-20211017154311447.png) #### 四次挥手 ![image-20211017161843566](readme.assets/image-20211017161843566.png) ### TCP传输可靠性 #### 传输可靠性 ![image-20211017162115784](readme.assets/image-20211017162115784.png) #### 数据片分段发送,ack,超时重发 ![image-20211017162445172](readme.assets/image-20211017162445172.png) ### 案例实操:(review代码) #### TCP传输初始化配置 1、初始化服务器端TCP监听 2、初始化客户端TCP发起连接操作 3、服务器端ServerSocket处理请求 #### 客户端与服务器交互 1、客户端发送简单字节 2、服务器端接收客户端发送数据 3、服务器回送数据,客户端识别回送数据 #### Nagle算法(拓展) ``` Small Packet Problem 在使用一些协议通讯的时候,比如Telnet,会有一个字节字节的发送的情景, 每次发送一个字节的有用数据,就会产生41个字节长的分组,20个字节的IP Header 和 20个字节的TCP Header, 这就导致了1个字节的有用信息要浪费掉40个字节的头部信息,这是一笔巨大的字节开销,而且这种Small packet在广域网上会增加拥塞的出现。 如果解决这种问题? Nagle就提出了一种通过减少需要通过网络发送包的数量来提高TCP/IP传输的效率,这就是Nagle算法 Nagle算法主要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他的小分组。 相反,TCP收集这些少量的小分组,并在确认到来时以一个分组的方式发出去。 算法流程: 1. 对于MSS的片段直接发送 2. 如果有没有被确认的data在缓冲区内,先将待发送的数据放到buffer中直到被发送的数据被确认【最多只能有一个未被确认的小分组】 3. 两种情况置位,就直接发送数据,实际上如果小包,但是没有未被确认的分组,就直接发送数据。 ``` #### 抓包:真实的三次握手 image-20211018191758704 #### 抓包:发送38位数据 ![image-20211018191930187](readme.assets/image-20211018191930187.png) #### 抓包:真实的四次挥手 ![image-20211018192130527](readme.assets/image-20211018192130527.png) wireshark抓包 ### 案例实操:基础类型数据传输 ![image-20211017212440319](readme.assets/image-20211017212440319.png) 1byte = 8 bit > byte的范围:-128~127 1、1char = 1byte 2、1short = 2byte 3、1boolean = 1byte 4、1int = 4byte 5、1long = 8byte 6、1float = 4byte 7、1double = 8byte 8、String.length byte 9、一个中文string = 3byte #### 流程 ``` 服务器端 1、创建ServerSocket 2、设置ServerSocket的参数,绑定IP和端口 3、等待客户端连接,如果有连接,则新建一个clientHandler线程 4、进入线程之后,等待客户端传入数据,调用inputStream读取数据,使用byteBuffer转换byte数组中的数据 5、该线程执行结束,关闭所有stream 客户端 1、创建socket,绑定到IP和端口 2、设置socket的参数,调用connect函数,连接服务器 3、使用bytebuffer封装数据,调用outputstream发送数据 4、使用Inputstream获取服务器传回来的数据 5、关闭所有资源 ``` ### TCP和UDP总结 > 1、TCP和UDP传输数据都是分组交换 > > 2、TCP采用inputStream和outputStream传输数据流(字节流) > > 3、UDP发送一个一个的报文,如果报文长度大于65535(32位),是否会把Packet分成两部分发送呢? > > ```java > // 构建一份请求数据 > // String requestData = MessageCreator.buildWithPort(LISTEN_PORT); > // byte[] requestDataBytes = requestData.getBytes(); > byte[] requestDataBytes = new byte[65536]; > Arrays.fill(requestDataBytes, (byte) 1); > > Exception in thread "main" java.net.SocketException: The message is larger than the maximum supported by the underlying transport: Datagram send failed > at java.base/java.net.DualStackPlainDatagramSocketImpl.socketSend(Native Method) > at java.base/java.net.DualStackPlainDatagramSocketImpl.send(DualStackPlainDatagramSocketImpl.java:136) > at java.base/java.net.DatagramSocket.send(DatagramSocket.java:695) > at UDPSearcher.sendBroadcast(UDPSearcher.java:67) > at UDPSearcher.main(UDPSearcher.java:21) > ``` > > ### UDP辅助TCP实现点对点传输案例 #### UDP搜索IP与端口 1、构建基础口令消息 2、局域网广播口令消息(指定端口) 3、接收指定端口回送的消息(得到客户端IP、Port) ![image-20211018151143858](readme.assets/image-20211018151143858.png) ##### 流程 ``` Server 1、封装UDPProvider为ServerProvider,设置sn,启动新的provider线程(单例模式) 2、在provider线程中,新建DatagramSocket,通过DatagramPacket接收Client广播的数据包Ⅰ,得到ip、port、cmd、UDP的响应端口。判断数据是否合法,若合法,则回送一个数据包Ⅱ,设置HEADER | cmd | port | sn 3、按下任何按键,退出ServerProvider Client 1、启动ClientSearch的searchServer方法,设置开始回送的定时器receiveLatch,设置listener,启动listen方法,发送广播 2、在sendBroadcast方法中,发送一个UDP的数据包Ⅰ,格式为:头部 | cmd | 端口信息,设置广播地址和UDP服务器的端口 3、在listen方法中,设置开始监听的计时器startDownlatch,启动一个listener线程,当异步线程listener完成之后,开始回送消息。 4、在listener线程中,收到Server回送的数据包Ⅱ,得到ip,port,cmd,serverport,sn,根据sn,构建一个serverinfo,添加到服务器信息列表中,通知receiveLatch可以继续往下运行,输出服务器列表,退出ClientSearch,回到主线程 ``` ##### 缺点 ``` 1、Server只能创建一个ServerProvider,同一时间只能对一个UDP客户端的搜索进行回应? 测试: 1、使用countdownlatch指定四个UDP客户端一起连接Server ``` #### UDP搜索取消实现 1、异步线程接收回送消息 2、异步线程等待完成(定时) 3、关闭等待-终止线程等待 ##### 改进 ``` Server 1、将L5-UDP中的ServerProvider恢复为UDPProvider,功能不变 2、添加了TCPServer,TCPServer中有一个ClientListener(当中有server.accpt(),之后转到ClientHandler),还有一个ClientHandler(异步线程,处理客户端的socket) 3、在ClientHandler中,使用PrintStream socketOutput和BufferedReader socketInput,发送和接收数据 Client 1、将UDPSearcher得到的结果传输给TCPClient的linkwith方法,添加了TCPClient类,进行TCP客户端的逻辑处理 2、在TCPClient类的linwith方法中,使用函数的参数ServerInfo,连接到对应的TCP的IP和端口 ``` ##### 流程 ``` Server 1、启动UDPProvider,当UDPSearcher发送广播报文,即可搜索到UDPProvider 2、调用TCPServer,等待TCPClient连接 3、accept()之后,可以开始TCP的数据发送和接收 Client 1、启动UDPSearcher,发送请求报文,可以搜索到UDPProvider,得到服务器的信息 2、根据服务器的信息,TCPClient连接对应的IP和端口 3、可以开始TCP的数据发送和回送 ```