简述
上一篇博客展示了如果A对b通过微信发了个“在吗”是多么的复杂,但事实上,我们写代码并不需要掌握其中的所有细节,操作系统给应用程序提供了一系列API,我们只需要按照对应的格式,就可以实现网络编程了
网络传输层中有许多的协议,其中最著名的是TCP和UDP,操作系统根据这两个协议,提供了两种不同的API
TCP和UDP区别
TCP的特点:有连接,可靠传输,面向字节流,全双工
UDP的特点:无连接,不可靠传输,面向数据报,全双工
有连接/无连接
有连接就是发送方和接收方必须建立好连接后才能互相通信,例如打电话,必须接受者接听了电话,两个人才能聊天,而无连接就相当于发微信,接受者不需要时时刻刻看微信才能收到消息
可靠传输/不可靠传输
由于网络环境十分复杂,稍微有一层的数据传输有问题,整体的传输就失败了,因此如果说发送方能够知道接受方收到消息了,就是可靠传输,否则就是不可靠传输
面向字节流/面向数据报
和我们之前讲的读写文件一样,传输数据也是有基本单位的,面向字节流就是以字节为基本单位进行传输,面向数据报就是以数据报为基本单位
全双工/半双工
全双工就是一个通道,双向通信
半双工就是一个通道,单向通信
我们的网络通信一半都是全双工的,一根网线可以同时上传和下载,也就是说一根网线是双向通信的
UDP socket
java提供了两个类,DatagramSocket,DatagramPacket
DatagramSocket是网卡的代言人,DatagramPacket代表一个UDP数据报
DatagramSocket
用来发送和接收UDP的数据报
构造方法
构造方法 | 说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
方法
方法 | 说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送 |
void close() | 关闭此数据报套接字 |
这里我们发现和文件的使用差不多,其实socket本质上也是文件,操作系统将各种硬件设备抽象成了文件,方便进行管理和调用,通过网卡发送数据,就相当于写文件,通过网卡接受数据就相当于读文件
DatagramPacket
是UDP发送和接收的数据报
构造方法
构造方法 | 说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
方法
方法 | 说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
UDP版本回显服务器,客户端
回显服务器也就是客户端发送什么数据,接受方就返回什么信息,也就是复读机
UdpEchoServer
package net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
public class UdpEchoServer {
private DatagramSocket socket = null;
/**
* 参数的端口代表服务器要绑定的端口
* @param port
* @throws SocketException
*/
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);//表示自己要连接哪个端口 服务器的Ip一般不用写——就是本机的Ip
}
/**
* 启动服务器
* @throws IOException
*/
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
//接收请求
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);//构造空的packet
//为Packet申请内存空间
socket.receive(requestPacket);
//输出型参数,构造一个空的DatagramPacket对象,网卡将数据填充到这个对象中
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//根据请求计算响应
String response = process(request);
//发送响应
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());//构造有数据的packet,使用InetAddress描述发送地址
socket.send(responsePacket);
//打印日志
System.out.printf("[%s:%d] req: %s; resp: %s",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
System.out.println();
}
}
/**
* 根据请求构造响应
* @param request
* @return
*/
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);//服务器端口号
udpEchoServer.start();
}
}
我们服务器的端口号可以从0到65535,但是0到1023一般都被著名的应用程序使用了,被称之为知名端口号,因此我们一般使用1024到65535
UdpEchoClient
package net;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();//系统会自动分配端口
this.serverPort = serverPort;
this.serverIp = serverIp;
}
private void start() throws IOException {
while(true){
//读取用户输入内容
Scanner scanner = new Scanner(System.in);
System.out.print("-> ");
String request = scanner.next();
//构造请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIp),this.serverPort);//构造有数据的packet,通过Ip和端口号描述地址
socket.send(requestPacket);
//读取服务器响应数据
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",9090);//目的服务器Ip——本机Ip,目的服务器端口
udpEchoClient.start();
}
}
Udp版本翻译服务器,客户端
本质上,翻译服务器就是通过大量存储英文和对应中文的键值对来实现翻译的功能,我们通过继承并重写回显服务器的process方法,可以实现翻译服务器的核心功能。
UdpTranslateServer
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpTranslateServer extends UdpEchoServer{
private Map<String, String> dict = new HashMap<>();
/**
* 参数的端口代表服务器要绑定的端口
*
* @param port
* @throws SocketException
*/
public UdpTranslateServer(int port) throws SocketException {
super(port);
dict.put("cat","猫");
dict.put("dog","狗");
dict.put("handsome","李骁");
}
//重写process方法
@Override
public String process(String request){
return dict.getOrDefault(request,"error");
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpTranslateServer(8090);
udpEchoServer.start();
}
}
UdpTranslateClient
和我们的回显客户端代码一致