TCP/UDP小记

文章目录

  • 前言
  • 1 TCP
    • 1.1 通讯时序
      • 1.1.1通讯时讯流程描述
      • 1.1.2 为什么要三次握手
      • 1.1.3 为什么要四次挥手
    • 1.2 滑动窗口
    • 1.3 状态转换
    • 1.4 分片问题
    • 1.5 JAVA服务器(多路复用)+客户端实现
    • 1.6 总结
  • 2 UDP
    • 2.1 通讯时序
    • 2.2 分片问题
    • 2.3 JAVA服务器(多路复用)+客户端实现
    • 2.4 总结


前言

TCP协议是一种作用在传输层的面向连接可靠的数据传输控制协议;
UDP协议是一种如用在传输层的无连接不可靠的数据传输控制协议;


1 TCP


TCP协议报文由源端口号和目的端口号,通讯的双方由IP地址和端口号标识。32位序号、32位确认序号、窗口大小稍后详细解释。4位首部长度和IP协议头类似,表示TCP协议头的长度,以4字节为单位,因此TCP协议头最长可以是4x15=60字节,如果没有选项字段,TCP协议头最短20字节。URG、ACK、PSH、RST、SYN、FIN是六个控制位

  1. SYN表示建立连接,FIN表示关闭连接;ACK表示响应;PSH表示有 DATA数据传输;RST表示连接重置;URG表示紧急指针字段有效。

  2. SYN、FIN、ACK位:其中ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应。如果只是单个的一个SYN,它表示的只是建立连接。TCP的几次握手就是通过这样的ACK表现出来的。但SYN与FIN是不会同时为1的,因为前者表示的是建立连接,而后者表示的是断开连接。

  3. RST位:RST一般是在FIN之后才会出现为1的情况,表示的是连接重置。一般地,当出现FIN包或RST包时,我们便认为客户端与服务器端断开了连接;而当出现SYN和SYN+ACK包时,我们认为客户端与服务器建立了一个连接。

  4. PSH位(Push):PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。TCP的连接建立和连接关闭,都是通过请求-响应的模式完成的。当两个应用进程进行交互式的通信中,有时在一端的应用程序希望在键入一个命令后立即收到对应的响应。在这种情况下,TCP就可以使用推送操作。通常的数据中都会带有PSH,但URG只在紧急数据才设置,也称“带外数据”。

  5. URG位(URGent):当URG=1时,表示紧急指针字段有效。他告诉系统次报文段有紧急指针,应该尽快的处理(相当于高优先级的数据),而不要按照原来的排序序列来传送。若不使用紧急指针,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有数据段被处理完毕后这两个字符才能被交付到接收方的网应用进程。URG是一个正偏移,与TCP首部中序号字段的值相加表示紧急数据后面的字节,即紧急指针是指向紧急数据最后一个字节的下一字节。

1.1 通讯时序

TCP/UDP小记_第1张图片

1.1.1通讯时讯流程描述

在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-11,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN, 8000(0), ACK11, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是11,
建立连接(三次握手)的过程:

  1. 客户端发送一个带SYN标志的TCP报文到服务器,这是三次握手过程中的第一段报文。SYN位表示连接请求。序号是10,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号11。
  2. 服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。ACK 11代表ACK位置1,32位确认序号是11“我已经接受到序列号10及其之前的全部包,请你下次发送序号为11的包”。
  3. 客户必须再次回应服务器端一个ACK报文,这是报文段3。对服务器的连接请求进行应答,确认序号是8001(单独的ACK确认包可以不携带32位序号)。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。
    数据传输的过程:
  4. 客户端发送32位序号是11,携带有效数据50字节,此处的ACK8001,是一个保障机制,预防段3 的ACK消息由于网络原因丢失。
  5. 服务器发送32位序号是8001,携带有效数据100字节,ACK位置为1,确认序号为61。“我已经接收到了11-60的全部包,请你下次发送序号为61的全部包”。
  6. 服务器发送32位序号是8101,携带有效数据100字节,ACK位置为1,确认序号为61。
  7. 客户端对服务器的响应做一个ACK确认,ACK位置为1,确认序号为8201。
    关闭连接(四次分手)的过程:
  8. 客户端发送FIN,61(0),FIN位表示关闭连接的请求
  9. 服务器回应ACK,应答客户端的关闭连接请求,确认序号位62因为FIN也占用一字节
  10. 服务器发送FIN,8201(0),其中也包含FIN位,向客户端发送关闭连接请求
  11. 客户端回应ACK,应答服务器的关闭连接请求,确认序号位8202因为FIN也占用一字节

1.1.2 为什么要三次握手

看下两次握手会产生什么影响就可以理解为什么要三次握手了。ClientA发送给ServerA 第一次发送SYN申请建立连接,但是因为网络原因阻塞ServerA没收到迟迟没有回复,于是第二次发送SYN申请建立连接,这次ServerA收到了并且回应了ACK给ClientA,在两次握手情景下此时连接建立,互相发送完请求和相应后正常关闭连接。但就在此时ClientA第一次发送的SYN数据包网络顺畅了到达了ServerA,但是ClientA已关闭甚至关机,但是由于两次握手ServerA回复ACK后建立连接,此时此连接会长期存在,会造成服务器资源浪费。

1.1.3 为什么要四次挥手

看下三次挥手会产生什么影响就可以理解为什么要四次挥手了,ClientA发送FIN给ServerA申请关闭连接,如果是三次挥手ServerA接到后就必须回复ACK+FIN 回复ClientA的关闭请求以及向ClientA发起关闭请求,然后ClientA回复ServerA的关闭请求发送ACK。在三次挥手的此次流程里有一个问题就是serverA必须同时回复ACK+FIN,在很多时候client申请关闭连接时ServerA还在处理之前请求或者为响应完数据,此时serverA不应该发送FIN,应该先发送ACK等数据发送结束后再发送FIN申请关闭。所以需要四次挥手才能满足以上场景。

下面再在套接字层面上解释一下四次挥手干了什么:
首先在Linux系统中一切皆文件,套接字也是一种特殊的文件为了建立网络通讯。欲建立连接的两个进程各自有一个socket来标识,即socket肯定时成对出现的,socket的包括读缓存区和写缓存区两部分。如下图,client的读缓存区对应server的写缓存区,client的写缓存区对应server的读缓存区。
TCP/UDP小记_第2张图片
在client发送FIN时就是告诉server“我要准备关闭啦,不会再给你发消息了,但是还能接收你的消息,你没发完的赶紧发”,server接收到以后回应ACK,此时client的写缓存区就关闭了,server的读缓存区关闭,此时称为半关闭状态。但是server还有消息没发完,等server发完消息以后,发送FIN给client“我消息发送完了,我也准备关闭了”,client收到以后回给server ACK,然后server的写缓存区和client的读缓存区关闭,连接真正关闭,释放资源。

1.2 滑动窗口

在UDP协议中如果客户端发送消息很快,服务器处理的比较慢会导致服务器的读缓存区满掉,后面的消息就回丢失,在TCP协议中利用“滑动窗口”这一机制解决了这个问题。

TCP/UDP小记_第3张图片

  1. 发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
  2. 发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
  3. 接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
  4. 接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
  5. 发送端发出段12,带有1K数据

从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的

1.3 状态转换

TCP/UDP小记_第4张图片

TCP/UDP小记_第5张图片

  • CLOSED:表示初始状态。
  • LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
  • SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
  • SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
  • ESTABLISHED:表示连接已经建立。
  • FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
    FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
    FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
  • FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。
  • TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
  • CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
  • CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
  • LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态

抛出一个疑问为什么主动发起的一方在发送完最后的ACK之后还要再等2ML时间后才能真正关闭?
答:当Client最后的ACK由于网络原因未到Server时,Server还会重新发送FIN申请关闭,此时如果Client已经关闭则Server会一直卡在LAST_ACK状态无法关闭,所以Client延时关闭就是为了防止最后ACK消失的情况。

1.4 分片问题

要想理解分片问题首先需要科普几个名词:分片、分段、MTU、MSS

  • MTU:以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的。这个1500字节被称为链路层的MTU(最大传输单元).

  • 分片:当传输层传入网络层的数据过大,加上网络层IP前缀后大于1500是,为了满足MTU限制,会在网络层对数据进行分片,使每一片都小于MTU,在接收端的网络层在进行重新组合,这样做的弊端是当其中一片数据包丢失时,导致无法组合,在UDP协议中此时如果上层(应用层)没有重传机制则只能丢弃全部数据,即使有重传机制也要将整个数据包进行重传(重传机制应用层肯定要为数据包做编号,应用层的编号只能加到数据报文的开始和结尾,如果触发了分片就算应用层设置了编号也不能确认那个分片丢失,需要整个数据报文重发),所以不管上层是什么协议尽量要避免网络层触发数据分片。

  • MSS:MSS就是TCP数据包每次能够传输的最大数据分段。TCP协议在连接建立阶段进行三次握手的时候会确认此次连接的MSS值,默认取通讯双方MSS的的最小值作为此次连接的MSS最大值即TCP数据包每次能够传输的最大数据分段。

  • 分段:分段是TCP的一个特性,当应用层传输的数据过大超过MSS时,TCP在传输层会进行分段,保证每个分段都小于MSS,TCP的每个分段都带有序号,在接收端的传输层可以进行重新排序组合,相比与网络层的分片组合,如果有一个分段消失,结合TCP的“确认和重传机制
    ”可以支持单个分段的重传而不用全部重传。

      看完上面四个名词的解释,大家应该也明白网络分片带来的影响不会出现的TCP协议中,因为MSS的值会小于MTU,所以如果传输层使用的是TCP协议到达网络层后都满足MTU,不用进行分片,所以在使用TCP协议时,无需在应用层(用户程序)去控制每次发送数据包的大小。
    

1.5 JAVA服务器(多路复用)+客户端实现

如果想对BIO/NIO/多路复用等IO模型有深入了解请看我上篇文章从系统调用分析IO模型演变,一篇讲懂BIO、NIO、多路复用(selector,poll,epoll)原理,保证一发入魂。
服务器

package com.lago;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class SelectorServer {
    public static void main(String[] args) throws Exception {
        // 创建Socket服务器,绑定端口8080(调用socekt()函数时第二个参数为SOCK_STREAM)
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 创建多路复用器selector :调用epoll_create函数创建eventpoll
        Selector selector = Selector.open();
        // 为socket服务器添加selector的引用:调用epoll_ctl将eventpoll添加至socket的等待队列中,并制定感兴趣的事件为有客户端连接时。
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);


        while (true){
            // 调用epoll_wait,如果eventpoll中的rdlist(readList)不为空不阻塞,如果为空阻塞,并且将main函数java进程
            // 添加至eventpoll的等待队列(进程挂起,释放cpu)
            int select = selector.select();

            // 如果有客户端连接服务器、或者收到客户端发来的消息,网卡会向cpu发送中断信号,触发中断程序唤醒java进程并且将有状态改变socket
            // 对象添加至rdlist。所以能走到这一步说明rdlist里必然有socket,这里获取eventpoll中rdlist里的socket集合。

            // rdlist中的socket处理完后不会清空,再有新的socket收到消息时会继续追加到rdlist中,为了防止socketA使用后
            // 还始终在rdlist中,rdlist在遍历处理完后程序显示清空一下。
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            for (SelectionKey selectionKey : selectionKeys) {
                if(selectionKey.isAcceptable()){
                    // 触发事件为有客户端连接,说明这个socket为服务端ServerSocketChannel
                    // 如果为服务端接收到新的连接,则获取新接入的客户端并且将客户端也添加selector的引用
                    ServerSocketChannel serverSocket = (ServerSocketChannel) selectionKey.channel();
                    // 获取新接入的客户端
                    SocketChannel client = serverSocket.accept();
                    // 为客户端也添加selector的引用:调用epoll_ctl将eventpoll添加至新接入客户端的等待队列中,并制定感兴趣的事件为接收到数据时。
                    client.configureBlocking(false);
                    client.register(selector,SelectionKey.OP_READ);

                    System.out.println("已经有客户端"+client.socket().getInetAddress().getHostAddress()+":"+client.socket().getPort());

                }else if(selectionKey.isReadable()){
                    // 触发事件为接收到数据,说明这个socket为客户端SocketChannel
                    SocketChannel socketClient= (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffers=ByteBuffer.allocateDirect(200);
                    // 获取客户端传入的消息
                    long read = socketClient.read(byteBuffers);

                    byteBuffers.flip();

                    String receiveData= Charset.forName("UTF-8").decode(byteBuffers).toString();

                    byteBuffers.clear();

                    System.out.println("接收到客户端"+socketClient.socket().getInetAddress().getHostAddress()+":"+socketClient.socket().getPort()+":"+receiveData);

                }
            }
            // 清空已经rdlist里已经经过处理的socket引用
            selectionKeys.clear();
        }
    }
}


客户端

public class Client01 {
    public static void main(String[] args) throws Exception {
        // 创建客户端设置服务器ip端口
        Socket socket = new Socket("192.168.2.170", 8080);
        Scanner scanner=new Scanner(System.in);
        while (true){
            String content = scanner.nextLine();
            socket.getOutputStream().write(content.getBytes());
        }
    }
}

1.6 总结

常规总结肯定是,TCP协议是一种作用在传输层的面向连接可靠的数据传输控制协议;但是这里的总结是结合上面所讲来分析这就话的加粗部分体现在哪儿。

  1. 面向连接:TCP在发送应用层真正的数据之前都会先进行三次握手创建连接,之后全部数据发送都是基于这连接进行发送。(三次握手在客户端调用connect函数时完成)
  2. 可靠:TCP之所以可靠,并不是因为面向连接才可靠(面向连接并不代表什么,关键是在建立连接时所作的准备比如:通信双方确认首次序号、确认滑动窗口大小、确认MSS大小、记录网络路由通道等),而是因为TCP具有确认和重传机制数据排序流量控制等机制。确认和重传机制体现在:当发送方发送的某个数据包在指定时间未接受到ACK回应时会重新发送;数据排序机制体现在:每个数据包都带有序列号,如果到达服务端是乱序,服务端可以根据序列号排序;至于流量控制机制滑动窗口就是TCP对它的体现。在使用TCP时这些机制都由内核帮我们实现不用我们操心。PS:下图只是方便大家理解这几个机制在哪里触发完成,解释可能不是特别恰当()!!,之前画的图了,懒得在画了,不是重点()!!
    TCP/UDP小记_第6张图片

2 UDP

TCP/UDP小记_第7张图片
UDP的报文相对于TCP来看简单了很多,因为UDP要做的事情也比TCP简单的多,甚至都不需要建立连接,在发送端只负责将应用层要发送的数据包丢给网络层,在接收端只需要将网络层中接收的数据包丢给应用层,不保证数据是否丢失。

2.1 通讯时序

TCP/UDP小记_第8张图片

由于UDP无需三次握手创建连接,也无需四次挥手断开连接,所以UDP的通讯时序图没什么意思,每条交互都是双方互发数据,没什么特别含义,这里用socket的系统调用图来展现UDP的交互细节,同时附一个TCP版的方便比较:

根据上图比较可看成,UDP客户端缺少了connect(),服务端缺少了accept(),在TCP的总结里也有提到connect函数触发了三次握手创建连接,这里也论证了UDP无需面向连接这一理论。

2.2 分片问题

注:在TCP的分片问题里介绍了MTU、MSS、分片、分段四个词的含义,忘记的同学可以往上翻翻。
UDP比起TCP就简单暴力的多,应用层给我什么数据我就一次性都给到网络层,并没有MSS等限制,所以在应用层一次想要发送大量数据时就会触发网络层的数据分片,然而UDP的接收端也不会考虑网络层重组的分片是否完整直接将数据给到应用层,所以应用层最好控制每次发送数据包的大小,保证到达网络层后数据包小于MTU不会触发网络分片(应用层的编号只能加到数据报文的开始和结尾,如果触发了分片就算应用层设置了编号也不能确认那个分片丢失,需要整个数据报文重发),除此之外需要为每个数据包设计编号,并且包含超时重发机制。

PS:UDP常说收到消息时顺序有可能错乱,并不是由分片引起的,分片是指发送端发送一段报文数据过大,被网络层进行分片处理,这里分片处理后到达接收端的网络层肯定可以按顺序排好序的;UDP乱序情景是发送端发送多段符合MTU的报文时,到达接收端后由于每个段报文走的路由可能不同可能导致多段数据到达顺序错乱,所以如果对数据展示顺序有严格要求时应用层最要有排序机制。同时也是因为UDP应用层需要考虑很多机制所以不可能到达一个报文展示一个报文,所以也推荐有缓存机制,接收端消息模块 处理完一组报文后放入缓存,接收端展示模块去读缓存中正确排序且完整的数据进行展示。

2.3 JAVA服务器(多路复用)+客户端实现

服务器
PS:此服务器为简单版UDP服务器,仅仅添加了个消息收到确认机制,并未添加超时重发、排序等机制。

public class SelectorUDPServer {
    public static void main(String[] args)throws Exception {
        // 创建UDP服务器,绑定端口,设置为非阻塞(调用socekt()函数时第二个参数为SOCK_DGRAM)
        DatagramChannel datagramChannel=DatagramChannel.open();
        datagramChannel.bind(new InetSocketAddress("192.168.2.170",8080));
        datagramChannel.configureBlocking(false);

        // 创建多路复用器:调用epoll_create创建eventpoll
        Selector selector = Selector.open();
        // 为socket服务器添加selector的引用:调用epoll_ctl将eventpoll添加至socket的等待队列中,并制定感兴趣的事件为有客户端连接时。
        datagramChannel.register(selector, SelectionKey.OP_READ);

        while (true){
            // 调用epoll_wait,如果eventpoll中的rdlist(readList)不为空不阻塞,如果为空阻塞,并且将main函数java进程
            // 添加至eventpoll的等待队列(进程挂起,释放cpu)
            int select = selector.select();
            if(select>0){
                // 如果有客户端连接服务器、或者收到客户端发来的消息,网卡会向cpu发送中断信号,触发中断程序唤醒java进程并且将有状态改变socket
                // 对象添加至rdlist。所以能走到这一步说明rdlist里必然有socket,这里获取eventpoll中rdlist里的socket集合。

                // rdlist中的socket处理完后不会清空,再有新的socket收到消息时会继续追加到rdlist中,为了防止socketA使用后
                // 还始终在rdlist中,rdlist在遍历处理完后程序显示清空一下。
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1480);

                for (SelectionKey selectionKey : selectionKeys) {
                    if(selectionKey.isReadable()){
                        // 触发事件为接收到数据,说明这个socket为客户端SocketChannel
                        DatagramChannel channel= (DatagramChannel) selectionKey.channel();
                        // 获取客户端传入的消息
                        InetSocketAddress receive = (InetSocketAddress) channel.receive(byteBuffer);
                        byteBuffer.flip();
                        String receiveData= Charset.forName("UTF-8").decode(byteBuffer).toString();
                        System.out.println("服务器接收到客户端"+receive.getAddress().getHostAddress()+":"+receive.getPort()+"发来的消息》》"+receiveData);
                        // 给客户端回复消息已收到
                        channel.send(ByteBuffer.wrap(("以接收到消息**"+receiveData+"**").getBytes()),receive);
                        byteBuffer.clear();
                    }
                }
                // 清空已经rdlist里已经经过处理的socket引用
                selectionKeys.clear();
            }

        }

    }
}

客户端
PS:客户端与服务器基本类似故未增加过度注释

public class UDPClient {
    public static void main(String[] args) throws Exception {
        DatagramChannel client=DatagramChannel.open();
        // 只是在javaAPI中为了流程统一调用了connect方法,其实并未调用系统函数connect
        client.connect(new InetSocketAddress("192.168.2.170",8080));
        Selector selector=Selector.open();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);

        while (true){
            Scanner scanner=new Scanner(System.in);
            String content = scanner.nextLine();
            client.write(ByteBuffer.wrap(content.getBytes()));
            int select = selector.select();
            if(select>0){
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                ByteBuffer byteBuffer=ByteBuffer.allocateDirect(1480);

                for (SelectionKey selectionKey : selectionKeys) {
                    if(selectionKey.isReadable()){
                        DatagramChannel channel= (DatagramChannel) selectionKey.channel();
                        InetSocketAddress receive = (InetSocketAddress) channel.receive(byteBuffer);
                        byteBuffer.flip();
                        String receiveData= Charset.forName("UTF-8").decode(byteBuffer).toString();
                        System.out.println("客户端接收到服务器"+receive.getAddress().getHostAddress()+":"+receive.getPort()+"发来的消息》》"+receiveData);
                        byteBuffer.clear();
                    }
                }
                selectionKeys.clear();
            }
        }
    }
}

2.4 总结

同TCP一样这里的总结的主要内容就是解析“UDP协议是一种如用在传输层的无连接不可靠的数据传输控制协议”中加粗字的含义体现在哪里。

  1. 无连接:从2.1通信时序中可以看到使用UDP协议时无需创建连接,无需三次握手,客户端可以直接给服务器发送消息。
  2. 不可靠:如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;如果发送多段数据包,并且在网络上经过不同的路由,到达接收端时顺序已经错乱了,UDP协议层也不保证按发送时的顺序交给应用层;通常接收端的UDP协议层将收到的数据放在一个固定大小的缓冲区中等待应用程序来提取和处理,如果应用程序提取和处理的速度很慢,而发送端发送的速度很快,就会丢失数据包,UDP协议层并不报告这种错误。说白了UDP在发送端只负责将应用层要发送的数据包丢给网络层,在接收端只需要将网络层中接收的数据包丢给应用层。完全套用底层的IP协议来传送报文,同IP一样提供不可靠的无连接数据包传输服务,不提供报文到达确认、排序、及流量控制等功能。

你可能感兴趣的:(网络编程,udp,tcp,网络编程,tcpip)