通过网络,让两个主机之间能够进行通信->就这样的通信来完成一定的功能,进行网络编程的时候,需要操作系统给咱们提供一组API,通过这些API来完成编程;API可以认为是应用层和传输层之间交互的路径,其中Socket Api(可以认为是插座)通过这个一套Socket Api可以完成不同主机之间,不同系统之间的网络通信;
传输层提供的网络协议主要有两个tcp、udp:这两个协议的特性(工作原理差异很大,就会导致使用这两种协议进行网络编程,也会存在一定的差异)
1、TCP是有连接的,UDP是无连接的;
这里的连接是抽象的概念,计算机中,这种抽象的连接是很常见的,此处的连接本质上就是建立连接的双方,各自保存对方的信息,即两台计算机建立连接,就是双方彼此保存了对方的关键信息;
TCP想要通信,就需要先建立连接(即计算机保存对方的信息),做完之后,才能后续通信(如果A和B想要建立连接,但是B拒绝了,通信就无法完成建立)
UDP想要通信,就直接发送数据报即可,不需要征得对方的同意,UDP自身也不会保存对方的信息(虽然UDP不知道,但是写程序的人得知道,UDP自己不保存对方的信息,但是调用UDP的socket api的时候就要把对方的位置信息等都传输过去)
2、TCP是可靠传输的,UDP是不可靠传输的;
网络上进行的通信,A->B发送一个信息,首先送到B的信息不可能做到100%送达,
所谓的可靠传输,就是退而求其次,当A->B发送一个消息,但是消息是不是达到B这一方,A这边自己是能感知到的,所以A在消息发送失败的时候就可以采取一定的措施(尝试重传之类的措施)
TCP就内置了可靠的传输机制,但是UDP就没有内置可靠传输;
可靠传输的不足:1、可靠传输所使用的机制更加复杂;2、可靠传输会导致消息的传输效率更加低;
综上所述:
TCP协议:发送方知道传输的数据是否成功的传输给接收方
UDP协议:消息发出去,就不管了,不再考虑消息是否成功发送给接收方;
3、TCP是面向字节流的,UDP是面向数据报的;
TCP也是和文件操作一样,以字节为单位来进行传输;UDP则是按照数据报(UDP有着自己严格的数据报格式)为单位来进行传输的
4、TCP和UDP都是全双工的;
一个信道,允许双向通信,就是全双工;一个信道,只能单向通信,就是半双工;
代码中使用一个socket对象,就可以发送数据也可以接收数据;
Socket其实是操作系统中的一个概念,本质上是一种特殊的文件;Socket就属于是把“网卡”这个设备给抽象成了文件了,往Socket文件里面写数据,就相当于通过网卡发送数据,从Socket文件里面读数据,就相当于通过网卡接收数据,(如此把网络通信和文件操作给统一了)
DatagramSocket :
Java中,就是用DatagramSocket这个类,来表示系统内部的socket文件;
DatagramPacket: 使用这个类,来表示一个UDP数据报;UDP是面向数据报的,每一次传输都是以UDP数据报为基本单位的;
写一个简单的UDP的客户端/服务器通信的程序
要求我们写的程序没有具体的业务逻辑,只是调用单纯的socket api,让客户端给服务器发送一个请求,且该请求是一个从控制台输入的字符串,服务器收到字符串之后,就会把这个字符串原封不动的返回给客户端,客户端在显示出来(即该服务器是回显服务器echo server)
1、读取请求并解析
服务器和客户端都需要创建socket对象,但是服务器的socket一般要显示的指定一个端口号,而客户端的socket一般不能显式指定(不显式指定,此时系统就会自动分配一个随机的端口),客户端的端口号是不需要确定的,交给系统进行随机分配即可,如果我们手动指定确定的端口,就可以和别人的程序的端口号冲突;
Q:服务器这边手动指定端口号,难道就不会出现端口号冲突吗?为啥客户端在意这个冲突,而服务器不在意这个冲突?
A:首先服务器是在程序员手里面的,一个服务器上有哪些程序,这些程序都使用哪些端口,程序员都是可控的,且程序员在写代码的时候,就可以指定一个空闲的端口,给当前的服务器使用即可;其次客户端的情况就不一样了,因为客户端是在用户的电脑上的,一方面,用户有成千上万,每一个用户电脑上所装的程序都不一样,占用的短端口也不一样,林外一方面,用户这边如果出现了端口号冲突,用户这方面自己本身不知道是什么原因
所以,客户端的端口号还是交给系统来进行分配,因为系统能保证肯定分配一个空闲的端口号;
服务器一旦启动,就会立即执行到这里的receive这里的方法,此时客户端的请求可能还没有过来,此时遇到这种情况,receive就会直接阻塞,一直阻塞到客户端把请求发送过来为止;
2、根据请求计算响应(一般的服务器都会经历的过程)
这个步骤是一个服务器程序最核心的步骤,我们当先的echo server不涉及到这些流程,只要求当请求过来就把请求当作响应,因为UDP是无连接的,所以udp自身不会保存数据要发送的对象信息,就需要每一次发送的时候,重新指定数据要发送到哪儿去;
3、把响应显示到客户端
4、打印一个日志,将我们进行的数据交互都打印出来
Q:为啥上述所写的代码中,没有写close,因为socket也是文件,不关闭的话就会出现之前我们所讲的文件资源泄露问题嘛?
A: 首先socket是文件描述符表中的一个表项,每一次打开一个文件,就会占用一个位置;文件描述符是在pcb上的(跟随的是进程)
其次这个socket在整个程序的运行中都是需要使用的(不能提前关闭),当socket不需要使用的时候,意味着程序就要结束了,进程结束,此时随之的文件描述符表就会销毁了(pcb也会销毁),被销毁后资源就会被系统进行自动回收;
最后,所谓出现泄露,是指代码中频繁打开文件,但是不会关闭,在一个进程的运行过程中,不断的打开文件,并逐渐消耗掉文件描述符表里面的内容,最终该资源就会被消耗殆尽了;但是,如果进程的生命周期很短,打开一下没多久就关闭了,谈不上所谓的资源泄露;
综上所述,文件资源泄露这样的问题,在服务器这边是比较严重的,在客户端这边其实影响不大;
2.3 代码实现
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
// 创建一个 DatagramSocket 对象. 后续操作网卡的基础.
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
// 这么写就是手动指定端口
socket = new DatagramSocket(port);
// 这么写就是让系统自动分配端口
// socket = new DatagramSocket();
}
public void start() throws IOException {
// 通过这个方法来启动服务器.
System.out.println("服务器启动!");
// 一个服务器程序中, 经常能看到 while true 这样的代码.
while (true) {
// 1. 读取请求并解析.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 当前完成 receive 之后, 数据是以 二进制 的形式存储到 DatagramPacket 中了.
// 要想能够把这里的数据给显示出来, 还需要把这个二进制数据给转成字符串.
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应(一般的服务器都会经历的过程)
// 由于此处是回显服务器, 请求是啥样, 响应就是啥样.
String response = process(request);
// 3. 把响应写回到客户端.
// 搞一个响应对象, DatagramPacket
// 往 DatagramPacket 里构造刚才的数据, 再通过 send 返回.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印一个日志, 把这次数据交互的详情打印出来.
System.out.printf("[%s:%d] req=%s, resp=%s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = "";
private int serverPort = 0;
public UdpEchoClient(String ip, int port) throws SocketException {
// 创建这个对象, 不能手动指定端口.
socket = new DatagramSocket();
// 由于 UDP 自身不会持有对端的信息. 就需要在应用程序里, 把对端的情况给记录下来.
// 这里咱们主要记录对端的 ip 和 端口 .
serverIp = ip;
serverPort = port;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 从控制台读取数据, 作为请求
System.out.print("-> ");
String request = scanner.next();
// 2. 把请求内容构造成 DatagramPacket 对象, 发给服务器.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 尝试读取服务器返回的响应了.
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 把响应, 转换成字符串, 并显示出来.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
//UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
UdpEchoClient client = new UdpEchoClient("42.192.83.143", 9090);
client.start();
}
}
分析如下:
1、服务期先启动,启动之后就开始进行循环,执行到receive这里并阻塞(此时还没有客户端过来)
2、客户端开始启动,也会先进入while循环,执行scanner.next,并且在这里进行阻塞,当用户在控制台输入字符串后,next就会返回,并且构造请求数据病发出来
3、客户端发出数据之后:
服务器:就会从rfeceive中返回,进一步的执行解析请求为字符串,执行process操作,执行send操作
客户端:继续往下执行,执行到receive服务器的响应;
4、客户端收到从服务器返回的数据之后,就会从receive中返回,执行这里的打印操作,也就把响应给显示出来了;
5、服务器这边完成过一次循环之后,又执行到receive这里;客户端这边完成一次循环之后,又执行到scanner.next这里,双双进入阻塞;
ps:本篇内容主要讲解关于UDP时间简单通信的过程,如果大家感兴趣的话就请一键三连哦!!!