1. 回显服务器——UDP
一个 UDP 的客户端/服务器通信的程序——回显服务器(echo server):
这个程序只是单纯地调用 Socket API
1)让客户端给服务器发送一个请求,请求就是从控制台输入的字符串
2)服务器收到字符串后,就会把这个字符串返回个客户端,客户端再显示出来
1. 服务器:
服务器和客户端都需要创建Socket对象
服务器的Socket一般要显示指定一个端口
客户端的Socket一般不能显示指定(系统自动分配一个随机的端口)
客户端的端口号是不确定的,交给系统进行分配即可
如果手动指定端口,可能会和其他程序的端口号起冲突
此时Socket对象就绑定到这个指定的端口
先构造一个空的对象,传递到方法内部,由receive内部对这个数据进行填充
DatagramPacket对象来承载从网卡读来的数据
接收数据的时候,需要一个内存来保存这个数据(DatagramPacket内部不能自行分配内存空间)
因此需要把空间创造好,交给DatagramPacket处理
此时服务器一旦启动,就会立即执行到这里的receive方法,客户端的请求可能还没来
receive会直接阻塞,直到客户端发来请求为止
String为取这个区间内的字节,构造成一个String
String request = new String(ruquestPacket.getData(),0,ruquestPacket.getLength());
此处的 ruquestPacket.getLength() 是收到数据的真实长度(取决于发送方发送了多少数据)
此处是服务器程序的核心步骤,但现在实现的是回显,相当于是把请求当成相应
UDP 是无连接的,每次发送的时候,重新指定数据要发送到哪里去
new DatagramPocket() 构造数据报,指定数据内容,也指定数据报要发给谁
response.getBytes(),response.getBytes().length 数据的内容
ruquestPacket.getSocketAddress() 请求的地址(IP地址和端口号)
response.getBytes().length
此处使用.getBytes(),是因为进行网络传输的时候,是使用字节的
如果是英文字符,字节和字符的个数是一样的,包含中文就不一样了(字符集)
此处是打印出一个日志
2. 客户端:
此处使用
String request = scanner.next();
是用为了判断不同的数据报,以换行符 /n 为标准
总结:
1)上述代码中为什么没有close
socket文件是文件描述符中的一个表项,每打开一个文件,就会占用一个位置
文件描述符,是在PCB上进行的
private DatagramSocket socket = null;这个socket是在整个程序运行过程中都是需要使用的(不能提前关闭)
当socket不需要使用的时候,程序也就要结束了
进程结束的时候,文件描述符就会随之摧毁(PCB摧毁)
随着销毁的过程,socket被系统自动回收
文件泄露的时机:
代码中频繁打开文件,但是不关闭
在一个进程的运行过程中,不断打开积累的文件,逐渐消耗文件描述符里的内容,最终消耗殆尽
如果进程的生命周期很短,打开一下没多久就关闭了,也不算泄露
文件泄露一般在服务器一边,在客户端影响不大
2. 3个 DatagramPacket 的构造方法:让数据报带上内容+数据的目的地址
a. 只指定字节数组缓冲区(服务器接受请求的时候使用,客户端接收响应的时候使用)
服务器返回响应给客户端
b. 指定字节数组缓冲区,同时指定一个 InetAddress 对象(这个对象同时包含IP和端口号)
c. 指定字节数组缓冲区,同时指定IP+端口号
需要把IP地址转换一下
1)服务器先启动,服务器启动后,就会进入循环,执行到receive这里并阻塞(此时客户端还没启动)
2)客户端启动,先会进入while循环,执行scanner.next(),并且在这里阻塞
当用户在控制台输入字符串后,next就会返回,从而构造请求数据并发送出来
3)客户端发出数据后:
服务器:从receive中返回,进一步的执行解析请求为字符串,执行process操作,执行sand操作
客户端:继续往下执行,执行到receive,等待服务器的响应
4)客户端收到从服务器返回的数据之后,就会从receive中返回
执行这里的打印操作,也就把响应显示出来了
5)服务器完成一次循环之后,又执行到receive这里
客户端完成一次循环之后,又执行到scanner.next()这里
双双进入阻塞
2. 词典——UDP
start方法中,调用process方法(this.process)
当前是子类引用调用start,this就是指向子类的引用。虽然this是父类的类型,但是实际指向的是子类引用,调用process,自然会执行到子类的方法
虽然没有修改start方法的内容,但是仍然可以确保按照新版本的process来执行
3. 回显服务器——CDP
两个关键的类:
1. ServerSocket 给服务器使用,使用这个类来绑定端口号
2. Socket 既会给服务器使用,也会给客户端使用
这两类都是用来表示socket文件的(抽象了网卡这样的硬件设备)
1)TCP 是字节流的,传输的基本单位是 byte
2)UDP 每次发送数据都得手动在send方法中指定目标的地址(UDP 自身没有存储这个信息)
TCP 不需要上述过程,前提是需要把连接给建立上
3)连接的建立不需要代码干预,是系统内核自动负责完成的
对应用程序来说:
客户端,主要是发起“建立连接”动作
服务器,主要是把建立好的链接从内核中拿到应用程序里
内核中有一个队列(可以视为一个阻塞队列)
如果有客户端,和服务器建立连接,这时服务器的应用程序是不需要做出任何操作的(无感知),内核直接完成建立连接的细节流程(三次握手)。完成流程后,就会在内核的队列中(这个队列是每一个serverSocket都有的一个队列)排队
应用程序想要和这个客户端进行通信,就需要通过一个accept方法把内核中队列中已经建立好的链接对象,拿到应用程序中
实现流程:
getPort() 得到对端的端口(客户端)
getInetAddress() 得到对端的IP(客户端)
getaLocalAddress() 得到本地的IP(服务器)
getLocalPort() 到本地的端口(服务器)
clientSocket.getInputStream()clientSocket.getOutputStream()InputStream 和 OutputStream 是字节流,可以通过这两个对象,完成数据的“发送”和“接收”
通过 InputStream 进行read操作——接收
通过 OutputStream 进行write操作——发送 以字节为单位进行传输
空白字符是一类特殊的字符——换行,回车符,制表符,翻页符,垂直制表符...
后续客户端发起请求,会以空白符作为结束标志(约定使用\n)
TCP 是字节流通信方式,每次传输/读取多少字节都很灵活
需要手动制定出,从哪到哪是一个完整的数据报
每循环一次处理一个数据报
在该过程中存在内存泄露—— clientSocket 这个对象没有进行 close
DatagramSocket 和 ServerSocket 都是在程序中的,只有这么一个对象,生命周期贯穿整个程序
clientSocket 则是再循环中,每次有一个新的客户端建立连接,都会创建出新的clientSocket
并且这个socket最多使用到该客户端退出(断开连接)
try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream())只是关闭了 clientSocket 上自带的流对象,并没有关闭socket本身
——>需要在try方法末尾,加上close,保证当前这里的socket能够被正确关闭掉
客户端的实现:
默认情况下,IDEA只允许一个代码创建一个进程
通过上述方法就可以在一个代码中同时创建多个进程
当启动两个客户端同时连接服务器:
其中一个客户端(先启动的客户端),一切正常
另一个客户端(后启动的客户端),无法与服务器建立按连接(服务器不会提示“建立连接”,也不会针对请求做出回应)
第一个客户端过来后,accept就返回了,得到一个clientSocket,进入processConnection
又进入一个while循环,在这个循环中,需要反复处理客户端发来请求数据。如果客户端这会没发请求,服务器的代码就阻塞在scanner.hasNext()这里
此时此刻,第二个客户端也来建立连接了,此时连接是成功建立的(内核负责)
建立成功后,连接对象就会在内核的队列里等待代码通过accept把连接取出来,在代码中处理
——>
第一个客户端会使服务器处于processConnection内部
进一步的也就使当前的第一层循环,无法第二次执行到accept
非得等到第一个客户端退出,processConnection才能结束,从而执行第二次accept
——>
创建新的新的线程,让新的线程调用processConnection
主线程就可以继续执行下一次accept了,新线程内部负责processConnection内部的循环
此时意味着,每次有一个客户端,就得分配一个新的线程
刚才出现这个问题的关键在于两重循环在一个线程里
进入第二重循环的时候,无法继续执行第一个循环
UDP 版本的服务器,当时只有一个循环,不存在类似的问题