目录
前言:
基础理解
传输层协议
UDP
TCP
Socket API
DatagramSocket API
DatagramPacket API
UDP实现回显服务器
完整代码展现(有详细注释)
UDP实现回显客户端
完整代码展现(有详细注释)
小结:
前言:
通过套接字Socket就可以实现客户端发送请求,服务起接收请求,处理完成后就可以响应给客户端。这样的一套流程就实现了数据在网络上的传输。
基础理解
网络编程中,在硬件上使用网卡发送和接收数据。在java中使用Socket直接操作网卡,而对于操作系统来说一切皆文件,那么这个Socket对象在操作系统中是被当作文件处理的。Socket就是操作系统给应用程序提供的接口。
Socket所提供的api和传输层密切相关,应用层首先接触的就是传输层。使用Socket所提供的api就可以实现应用层的代码并且和传输层进行交互。
客户端发起请求 --> 服务器接收请求 --> 服务器处理请求并响应给客户端 --> 客户端接收响应
传输层协议
UDP
特点:无连接,不可靠传输,面向数据报,全双工,大小首先(一次最多64k),有接收缓冲区无发送缓冲区。
TCP
特点:有连接。可靠传输,面向字节流,全双工,大小不限,有接收缓冲区和发送缓冲区。
理解:
1)无连接:不需要建立客户端和服务器之间的连接,就可以发送数据。(例如微信发消息)
2)有连接:需要建立客户端和服务器之间的连接,才可发送数据。(例如打电话,需要接听)
3)不可靠传输:发送方不知道数据是发过去了,还是丢包了。
4)可靠传输:发送方知道自己的消息是否发送过去。
注意:可靠性就是针对发送方是否清楚数据是否发送过去。
5)面向数据报:数据传输以“数据报”为基本单位,一块一块的发数据。
6)面向字节流:数据传输和读文件类似,“流式”的。一次发送部分数据,也可以发送全部数据。
7)全双工:可以同时发送和接收数据,那么半双工就不支持。
Socket API
java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用
DatagramPacket 作为发送或接收的UDP数据报。
DatagramSocket API
DatagramSocket构造方法
注意:
创建一个UDP数据报套接字的Socket,绑定本机任意一个随机端口(一般用于客户端)
注意:
创建一个UDP数据报套接字的Socket,绑定指定端口(一般用于服务端)
DatagramSocket方法
注意:
从网卡接收数据报。这个参数需要一个空的DatagramPacket对象,当从网卡接收到数据报就会填充好这个空的对象,以便供我们处理数据。
如果没有接收数据报,这个方法会阻塞等待。
注意:
将已经构造好的数据报发送到网卡。不会阻塞等待直接发送。
注意:
在操作系统中Socket对象是被当作文件处理的,那么就需要释放pcb中文件描述符表中的资源。
DatagramPacket API
DatagramPacket构造方法
注意:
构造一个DatagramPacket以用来接收数据报,接收的数据保存在buf缓冲数组中,接收的指定长度。
注意:
构造一个DatagramPacket以用来接收数据报,数据填充为字节数组,从0起始位置到指定长度(offset,length),address指定目的主机IP和端口号。(一般处理完请求后,构造成数据报来发送)
DatagramPacket方法
注意:
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址。
注意:
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号。
注意:
获取数据报中的数据,就是缓冲数组。
UDP实现回显服务器
服务器是被动的一方,需要接收客户端发起的请求。那么客户端就必须明确服务器的ip和具体进程的端口号。所以在实现服务器时就必须指定端口号,这里实现的是本机到本机的数据发送,ip就使用环回ip即可。
由于不清楚客户端什么时候发起请求,那么服务器不能休息(随时待命)。这里使用死循环的方式,但它不会一直循环,因为receive()方法当没有接收到请求时会阻塞等待。
我们首先需要明确服务器的工作流程。接收客户端的请求 --> 处理请求 --> 将响应发送给客户端。
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
注意:
首先构造一个空的DatagramPacket对象,传入缓冲数组,和指定长度。当下面receive()方法从网卡接收到客户端请求时就会填充这个空对象。(数据是写入了缓冲数组)
当receive()方法当没有接收到请求时会阻塞等待。(随时待命)
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
注意:
由于接收的数据构造成了数据报,这样不利于我们处理数据。我们将数据报中的数据取出来构造成字符串。
String response = process(request);
public String process(String request) {
return request;
}
注意:
服务器针对请求进行需求处理,这里的process是一个方法。由于我们实现的是回显服务器,即直接返回这个字符串即可。
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
注意:
这里将处理完后的响应构造成数据报,然后发送给客户端程序。这里需要传入字节数组,填充的具体长度,(这里需要用字节数组的长度,不能用字符串的长度。转换之后两者长度是不一致的)和客户端的ip和端口号(getSocketAddress()方法可以获得发送方的ip和端口号)。
当构造完成之后直接将数据报发给客户端即可。
完整代码展现(有详细注释)
public class UdpEchoSever {
//Socket对象直接操作的是网卡,在操作系统中任务Socket对象是文件(一切皆文件)
//通过Socket对象接收和发送数据
private DatagramSocket socket = null;
public UdpEchoSever(int port) throws SocketException {
//服务器是被动的一方,客户端必须找到服务器的端口,才能找到指定程序,因此服务器必须指定端口号
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
//构造空的Packet对象,传入缓冲数组
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
//receive从网卡接收数据,解析后填充这个空对象(输出形参数)(可以认为写入了缓冲数组)
//客户端如果没有发请求receive就会阻塞,直到客户端发送请求(保证这里不会一直循环)
socket.receive(requestPacket);
//根据接收的数据(由于接收的数据不方便处理),因此构造成字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//服务器响应处理
String response = process(request);
//构造发送的数据报,字节数组,字节数组长度,IP和端口(根据响应的字符串)
//这个DatagramPacket只认字节数组,因此就需要获取字节数组的长度而不是字符的个数
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
//发送数据到Ip和端口指定的客户端程序
socket.send(responsePacket);
//打印下,请求响应的中间结果
System.out.printf("源IP:%s 源端口:%d 请求数据:%s 响应数据:%s\n", requestPacket.getAddress().toString(),
requestPacket.getPort(), request, response);
}
}
//回显服务器,处理直接返回数据(响应)
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
//端口号的指定在 1024 -- 65535 里指定
UdpEchoSever sever = new UdpEchoSever(8280);
sever.start();
}
}
UDP实现回显客户端
客户端发送数据需要明确服务器的ip和具体进程的端口号。客户端的端口号我们不需要手动指定,因为客户端程序是存在于客户主机上,我们如果手动指定就很可能与其他进程端口号冲突,这样就直接抛异常了(Address already in use)。直接让操作系统随机分配一个空闲的端口号。
那么为什么服务端我们可以指定端口号,这样就不怕与其他进程冲突了么?因为服务器在我们自己手里,我们明确里面的各种端口号,简单来说就是可控的。
我们首先明确客户端的工作流程。用户输入数据 --> 发送到服务器 --> 接收服务器的响应。这里也使用死循环,和上面一样receive()方法会阻塞,不会一直循环。
Scanner scanner = new Scanner(System.in);
System.out.println("输入你要发送的数据:");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("bye bye");
break;
}
注意:
提示用户输入数据,这里做了简单的判断。
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(severIp), severPort);
socket.send(requestPacket);
注意:
根据用户输入的数据构造成数据报。需要字节数组,具体填充的长度(同样的需要字节数组长度而不是字符串长度),ip(由于这里需要一个32位的ip,而上面的是字符串,因此需要转换)和服务器端口号。然后直接发送即可。
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
注意:
接收服务器的响应。首先构造一个空的DatagramPacket对象,传入缓冲数组和指定长度。receive()方法从网卡接收到数据报然后构造好这个空的对象。
receive()当没有接收到响应前同样的也会阻塞等待。
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
注意:
由于是数据报不利于用户观察数据,因此转转换为字符串。获取到数据报中的缓冲数组,从0位置到指定长度来构造这个字符串,最终显示给用户。
完整代码展现(有详细注释)
public class UdpEchoClient {
private DatagramSocket socket = null;
//客户端需要知道服务器的IP,和端口,这里先存一下
private String severIp = null;
private int severPort = 0;
public UdpEchoClient(String severIp, int severPort) throws SocketException {
//客户端不需要指定端口号,客户端程序在用户手里,指定端口号就可能和其他进程重复。因此让操作系统分配一个空闲的端口
//服务器为什么指定端口不怕重复呢?因为服务器在程序员手里我们清楚端口号的使用(可控的),而客户端是(不可控的)
socket = new DatagramSocket();
this.severIp = severIp;
this.severPort = severPort;
}
//客户端启动
public void start() throws IOException {
//用户输入数据
while (true) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入你要发送的数据:");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("bye bye");
break;
}
//发送数据报(构造DatagramPacket对象)
//此处的IP需要一个32位的整数,而上面的是字符串,需要转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(severIp), severPort);
socket.send(requestPacket);
//接收数据报(填充这个空对象)(阻塞到服务器发送过来数据)
//receive的阻塞操作系统实现的,JAVA只是封装了一下
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
//显示数据报(将数据报转换为字符串)
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 8280);
udpEchoClient.start();
}
}
小结:
这里大多是api的使用,我们要理解其中的原理,便能得心应手。