【Java ee初阶】网络编程 UDP socket
网络编程
socket api 是传输层提供的api。
UDP 无连接,不可靠传输,面向数据报,全双工。
TCP 有链接,可靠传输,面向字节流,全双工。
UDP socket api 数据报
DatagrammSocket 代表了操作系统中的socket文件。
文件是“硬盘”硬件设备的抽象,读写文件的时候,本质上是在操作硬盘。实际上,文件在操作系统中,是更广义的概念,可以代表更多的硬件设备和软件资源。
例如,标准输入和标准输出,对应的文件就是System.in和System.out。
网卡,网卡硬件设备也是通过文件来进行封装的。通过网络发送数据,需要往网卡中写入。通过网络接收数据,需要从网卡中读取。
封装网卡的文件,我们称为“socket 文件”
使用的时候要进行的操作:
1.打开
2.读写
3.关闭
Java中创建一个DatagramSocket对象,就是在操作系统中打开了一个socket文件,这样子的socket文件,就代表了网卡。
通过这个对象写入数据,就是在通过网卡发送数据。通过这个对象读取数据,就是在通过网卡接收数据。
UDP数据报套接字编程
API介绍
DatagramSocket
DatagramSocket是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket构造方法:
UDP数据报套接字方法表
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket方法
数据报套接字方法表
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
DatagramPacket
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket构造方法:
DatagramPacket方法说明表
方法签名 | 方法说明 |
DatagramPacket(byte[]buf,int length) | 构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[]buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
DatagramPacket方法:
网络编程方法签名及说明表
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
这个对象本质上就是包装了字节数组。
网络通信程序的基本流程[通用的流程]
这个模型为一问一答模型,其实还有其他的模型,因为对于服务器来说,通常是要给多个客户端提供服务的。
接下来写一个最简单的网络程序。
服务器
1. 从客户端读取到请求内容
2. 根据请求计算响应
3. 把响应返回给客户端
客户端
1. 从控制台读取用户输入的内容
2. 通过网络发送给服务器
3. 从服务器读取到响应
4. 把响应结果显示到控制台上 不同的服务器,根据请求,计算出来的响应是不一样的,具体的根据业务逻辑去实现。
此处就做一个简单的“回显服务器”(echo server) 省略了根据请求计算响应部分,而是把请求直接作为响应进行返回。
关注要点:
1. socket api的使用
2. 客户端服务器的工作流程
网络通信中关于套接字(socket)绑定的端口号谁是源端口,谁是目的端口?需要具体情况具体分析。
我们已经知道了网络通信五元组为: 源ip,源端口,目的ip,目的端口,协议类型
而服务器在启动的时候,绑定一个端口号(假设9090,咱们可以随意指定的)
客户端在启动的时候,绑定一个端口号(假设1234)
客户端给服务器发的请求, 源端口就是1234,目的端口就是9090
服务器给客户端返回的响应 源端口就是9090,目的端口就是1234
我们需要指定的是“空闲端口”
一个端口同一时刻,只能被一个进程(socket) 绑定
假设,微信和QQ音乐绑定了同一个端口,此时网卡收到了这个端口的数据,要交给哪个程序来处理呢?操作系统是不支持这个操作的。
如果在绑定端口的时候,该端口已经被其他进程占用了,此时绑定就会失败,也就是抛出异常。
端口号在网络协议中,是使用 2 个字节表示的无符号整数, 范围为0 - 65535
而0-1024 这个范围的端口,称为“知名端口号” 。这些端口号被系统预留了,给一些知名的协议的服务器来进行使用的。
我们自己写代码最好避开这个范围。
那么怎么知道哪些端口被占用了? 又如何知道,当前系统中,正在运行的程序都使用了哪些端口呢?
此时有两种方法供我们选择:
1) 尝试绑定一下,成功就ok,不成功就换个。
2) 通过命令查看端口的情况. windows 中通过 netstat 命令查看 netstat -ano
*通过观察可知,tcp更多。其实tcp和udp都可能更多。 取决于电脑上都装了哪些程序, 但是事实上,tcp确实比udp功能更强,大部分程序使用tcp协议,所以tcp更多也是可以理解的。
*端口号是2个字节但是用int接收, 它会检查我们输入的大小吗?系统的原生API不会检查,但是如果设置的比较大,会直接截断。 Java的API 确实是检查了:
由于服务器要给多个客户端提供服务,并且每个客户端都可能发起多个请求。 所以我们要使用while循环,来持续不断的处理各种客户端的各种请求。 死循环,不见得是bug。尤其是对于服务器,主逻辑往往就是一个死循环。
其中 p是一个输出型参数
编写 udp 版本的 echo server
服务器一启动,就会执行到 receive.
如果此时还没有客户端,发送任何请求,receive 就会阻塞。
其中responseString.getBytes().length是传入字节数组的长度,以字节为单位。
构造这个 DatagramPacket 的时候,需要拿着 String 里面的字节数组来进行构造。
千万不能写作response.length ();因为这个是以字符为单位
进行send的时候,要把这个packet发给谁呢? 一个服务器,对应多个客户端
原则上,这个请求是谁发的,我就返回给谁。
而 UDP是“无连接”,也就是说UDP socket自身,里面不保存对方的信息的
此时就需要把 发给谁这样的信息,放到“packet”中
一次通信,涉及到一个“五元组”
源IP,源端口 目的IP,目的端口 协议类型
上次谈到的“封装”“分用”
服务器收到的客户端的请求,不是光一个UDP数据报
UDP外面有IP IP外面有以太网数据帧……
request.getSocketAddress()这个方法,拿到了这个数据包对应的客户端的 IP和端口。
调用receive操作收到的其实是一个完整的这样的“数据”。虽然以太网报头和IP的报头,都被操作系统解析了。 但是仍然可以通过DatagramPacket 类获取到其中的关键信息。
把这里的客户端中的ip和端口拿出来,用来构造响应数据报
这里的代码可以体现UDP的几个特点:
1.UDP无连接
一上来就可以直接receive /send
没有建立连接的代码
2.面向数据报
3.全双工
*一些区别,request.getSocketAddress拿到ip和端口的整体,而reqPacket.getAddress只获取ip。
package network.UDP;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class EchoServer {//创建socket对象private DatagramSocket socket;//创建构造方法,初始化socket对象public EchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}//启动服务器,完成主要的业务逻辑public void start() throws IOException{System.out.println("服务器启动!");while (true) {//1.读取请求并进行解析//(1)创建一个空白DatagramPacket对象DatagramPacket request = new DatagramPacket(new byte[4096], 4096);//(2)通过receive方法读取网卡的数据,如果网卡没有收到数据,就会阻塞socket.receive(request);//(3)解析数据,得到请求的内容String requestString = new String(request.getData(), 0, request.getLength());//2.根据请求计算响应String responseString = process(requestString);//3.把响应写回客户端//(1)创建一个DatagramPacket对象,封装响应内容DatagramPacket response = new DatagramPacket(responseString.getBytes(), responseString.getBytes().length,request.getSocketAddress());//(2)把DatagramPacket对象通过send方法发送给客户端socket.send(response);//4.打印日志System.out.printf("[%s:%d] req: %s; resp: %s\n", request.getAddress().toString(), request.getPort(), requestString, responseString);}}//根据请求内容,返回响应内容//由于我们是“回显服务器”,所以响应内容就是请求内容private String process(String requestString) {return requestString;}public static void main(String[] args) throws IOException {EchoServer echoServer = new EchoServer(9090);echoServer.start(); }
}
编写 udp 版本 的 echo client
客户端的构造方法,需要填写服务器的ip和端口。
服务器则不需要,只需要指定自己的端口。
作为服务器,发数据的时候,就可以通过收到的请求,直到是要发给谁。
作为客户端,主动发起的一方,必须得实现知道服务器在哪里。(程序员手动指定的)
客户端在创建socket对象的时候,不需要指定端口号, 操作系统会自动分配一个空闲的端口
客户端这里指定也可以,但是通常不会这么做, 因为一个端口,同一时刻,只能被一个进程绑定。(第二个进程也尝试绑定,就会出错)
对于服务器来说,由于服务器是在程序员自己手里。如果出现端口冲突了,程序员很容易就能处理
而 对于客户端来说,是在用户自己的电脑上的。(用户电脑上可能会安装很多奇奇怪怪的程序,占用各种奇奇怪怪的端口,程序员管不着,也不能管) 所以,如果客户端里指定固定端口,就很可能会和客户端电脑上的其他程序的端口冲突。因此通常不需要指定端口号。
package network.UDP;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class EchoClient {private DatagramSocket socket = null; //创建socket对象,用于发送和接收数据报private String serverIp; //服务器的IP地址private int serverPort; //服务器的端口号public EchoClient(String serverIp, int serverPort) throws SocketException { //构造方法,初始化socket对象this.serverIp = serverIp; //初始化服务器IP地址this.serverPort = serverPort; //初始化服务器端口号socket = new DatagramSocket(); //创建DatagramSocket对象,用于发送和接收数据报}public void start() throws IOException { Scanner scanner = new Scanner(System.in); //创建Scanner对象,用于读取用户输入System.out.println("客户端启动!"); while (true) { System.out.println("->"); String requestString = scanner.nextLine(); DatagramPacket request = new DatagramPacket(requestString.getBytes(), requestString.getBytes().length,InetAddress.getByName(serverIp),serverPort); socket.send(request); //发送数据报DatagramPacket response = new DatagramPacket(new byte[4096], 4096); //创建DatagramPacket对象,用于接收数据报socket.receive(response); //接收数据报String responseString = new String(response.getData(), 0, response.getLength()); //解析数据报,得到响应内容System.out.println(responseString); //打印响应内容}}public static void main(String[] args) throws IOException {EchoClient client = new EchoClient("127.0.0.1", 9090);client.start();}}
环回IP 英文名叫loopback 表示自己这个主机。 当服务器和客户端在同一个主机上的时候 无论主机真实的ip是啥,都可以通过127.0.0.1来访问服务器。
如果启动出现以下错误,说明端口已经被占用了
编写 udp 版本的字典客户端和字典服务器
package network.UDP;import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;public class DictServer extends EchoServer{private Map<String,String> dict = new HashMap<>();public DictServer(int port) throws SocketException {super(port);dict.put("cat", "猫");dict.put("dog", "狗");dict.put("pig", "猪");dict.put("cow", "牛");dict.put("sheep", "羊");dict.put("chicken", "鸡");dict.put("duck", "鸭");dict.put("goat", "羊");dict.put("horse", "马");dict.put("elephant", "大象");dict.put("monkey", "猴");dict.put("tiger", "老虎");dict.put("lion", "狮子");dict.put("giraffe", "长颈鹿");dict.put("zebra", "斑马");dict.put("bear", "熊");dict.put("fox", "狐狸");dict.put("wolf", "狼");dict.put("deer", "鹿");dict.put("rabbit", "兔子");dict.put("mouse", "老鼠");dict.put("hamster", "仓鼠");dict.put("fish", "鱼");}@Overridepublic String process(String requestString) {return dict.getOrDefault(requestString, "未找到该单词");}public static void main(String[] args) throws IOException {DictServer dictServer = new DictServer(9090);dictServer.start();}}
package network.UDP;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class EchoClient {private DatagramSocket socket = null; //创建socket对象,用于发送和接收数据报private String serverIp; //服务器的IP地址private int serverPort; //服务器的端口号public EchoClient(String serverIp, int serverPort) throws SocketException { //构造方法,初始化socket对象this.serverIp = serverIp; //初始化服务器IP地址this.serverPort = serverPort; //初始化服务器端口号socket = new DatagramSocket(); //创建DatagramSocket对象,用于发送和接收数据报}public void start() throws IOException { Scanner scanner = new Scanner(System.in); //创建Scanner对象,用于读取用户输入System.out.println("客户端启动!"); while (true) { System.out.println("->"); String requestString = scanner.next(); DatagramPacket request = new DatagramPacket(requestString.getBytes(), requestString.getBytes().length,InetAddress.getByName(serverIp),serverPort); socket.send(request); //发送数据报DatagramPacket response = new DatagramPacket(new byte[4096], 4096); //创建DatagramPacket对象,用于接收数据报socket.receive(response); //接收数据报String responseString = new String(response.getData(), 0, response.getLength()); //解析数据报,得到响应内容System.out.println(responseString); //打印响应内容}}public static void main(String[] args) throws IOException {EchoClient client = new EchoClient("127.0.0.1", 9090);client.start();}}