目录
前言
我们写网络程序, 主要编写的是应用层代码.
真正要发送这个数据, 还需要上层协议调用下层协议, 也就是应用层调用传输层.
传输层给应用层提供了一组 api, 统称为 socket api, 系统给程序提供的 api 是C 风格的, JDK 针对这些 api 进行封装, 封装成 Java 风格的 api.
提供的 socket api 主要是这两组 :
- 基于 UDP 的 api
- 基于 TCP 的 api
因为 UDP 与 TCP 的协议差别很大, 所以这两组 api 差别也很大.
那这两个协议都有啥特点呢?
UDP :
- 无连接 (使用 UDP 的双方不需要刻意保存对端的相关信息)
- 不可靠传输 (消息发送完了就行, 不关注结果)
- 面向数据报 (以一个 UDP 数据报为基本单位)
- 全双工 (一条路径, 双向通信)
TCP :
- 有连接 (使用 TCP 的双方要刻意保存对端的相关信息)
- 可靠传输 (发送消息后, 知道对方是否接收到)
- 面向字节流 (以字节为传输的基本单位, 读写方式非常灵活)
- 全双工 (一条路径, 双向通信)
UDP 版的回显服务器
需要用到的 api
- DatagramSocket API
DatagramSocket 是 UDP Socket,用于发送和接收UDP数据报。
主要用到的构造方法 :
DatagramSocket()
创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(intport)
创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
主要用到的方法 :
void receive(DatagramPacket p)
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacketp)
从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()
关闭此数据报套接字
- DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报.
主要用到的构造方法 :
DatagramPacket(byte[] buf, int length)
构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数 length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从offset到指定长度(第三个参数length)。address指定目的主机的IP和端口号
主要用到的方法 :
InetAddress getAddress()
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址.
int getPort()
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获
取接收端主机端口号
byte[] getData()
获取数据报中的数据
构造UDP发送数据报时,需要传入 SocketAddress ,该对象可以使用InetSocketAddress 来创建.
服务端
public class UdpEchoServer {
//首先定义一个 socket 对象, 通过 socket 对象来发送读取信息
private DatagramSocket socket = null;
//绑定一个端口, 如果绑定的端口被别的进程占用了, 这里就会报错.
//同一个主机上, 一个端口只能被一个进程绑定.
public UdpEchoServer(int port) throws SocketException {
//构造时, 指定要绑定的端口号.
socket = new DatagramSocket(port);
}
//启动服务器的主逻辑
public void start() throws IOException {
System.out.println("服务器启动!!!");
//因为服务器是要时刻读取客户端信息的, 所以使用while循环来重复读取并处理信息.
while(true) {
//每次循环都只做三件事
// 1.读取请求并解析
// 下面这是构造一个数据包, 就可以理解为一个餐盘, 这个餐盘里指定要放入的数据类型及大小
// 空的餐盘构建好了就得装东西了
DatagramPacket requestPacket = new DatagramPacket(new byte[666], 666);
// 1kb=1024byte, UDP最多发送64kb(包含UDP首部8byte)
// 通过 socket 对象来接收信息(也就是填充餐盘)
socket.receive(requestPacket); //如果没接收到信息就会阻塞等待
// 为了方便我们处理这个请求, 将数据包转为 String
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
// 2.根据请求来计算响应(这里直接返回了)
String response = process(request);
// 3.把响应结果写回客户端
// 根据 response 来构造一个 DatagramPacket
// 和之前构造不同, 本次构造相当于将餐盘填满, 注意还要指定发送给谁
// requestPacket.getSocketAddress()是获取客户端的IP和端口号(填充的 requestPacket 就包含了客户端的IP和地址)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
0,response.getBytes().length,requestPacket.getSocketAddress());
// 把结果发送给客户端
socket.send(responsePacket);
//打印一下客户端的IP和端口号, 还有请求和响应
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 echoServer = new UdpEchoServer(8888);
echoServer.start();
}
}
客户端
public class UdpEchoClient {
//客户端也需要 socket 对象来进行数据交互
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//构造客户端时, 还需要知道服务器在哪, 才能去获取服务
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
//不同与服务器, 这里没有关联端口, 不代表不需要关联端口
//而是系统会自动为客户端分配个空闲的端口
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("启动客户端!!!");
//通过 Scanner 来读取用户输入
Scanner scanner = new Scanner(System.in);
while(true) {
// 1.先从控制台读取一个字符串
// 打印一个提示符, 提示用户输入
System.out.print("-> ");
String request = scanner.next();
// 2.把字符串构造成 UDP packet, 并进行发送
// InetAddress.getByName() 确定主机的IP地址
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3.客户端尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[666],666);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req: %s, resp: %s\n", request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",8888);
udpEchoClient.start();
}
}
启动看看效果 :
客户端 :
服务端 :
UDP 版本的字典客户端和字典服务器
要想实现单词查找功能, 首先要有一个数据结构来对单词进行存储, 一般用哈希表来存储, key 来存储单词, value 存储单词意思.
其实客户端都是一样的, 只是服务器根据请求, 返回的响应不一样了.
所以我们主要就是重写 process 方法.
服务端 :
public class UdpDictServer extends UdpEchoServer{
private HashMap<String,String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("pen","笔");
dict.put("dog","狗");
dict.put("cat","猫");
//...可以无限添加, 其实网上的翻译词典什么的,就是样本比这个大
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"未找到该单词.");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer = new UdpDictServer(8888);
udpDictServer.start();
}
}
客户端 :
服务端 :
TCP 版的回显服务器
需要用到的 api
- ServerSocket API
ServerSocket 是创建TCP服务端 Socket 的 API (给服务器用的)
ServerSocket 一定要绑定具体端口号. (服务器得绑定的端口号才能提供服务)
主要用到的构造方法 :
ServerSocket(int port)
创建一个服务端流套接字Socket,并绑定到指定端口.
主要用到的方法 :
Socket accept()
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待.
void close()
关闭此套接字 (大多情况下, ServerSocket 的生命周期都会跟随整个程序, 所以不调 close 也问题不大)
accept 就是接受的意思.
客户端主动发起连接, 服务端被动接受连接.
其实 tcp 的连接是在内核就完成了的, 这里的 accept 是应用层层面的接受, 并返回一个 Socket 对象, 通过这个对象就可以与客户端进行交互了.
- Socket API
Socket 即会给服务端用, 也会给客户端用.
不管是客户端还是服务端Socket,都是双方建立连接以后,保存对端信息,用来与对方收发数据的。
Socket 和 DatagramSocket 类似, 都是构造的时候指定一个具体的端口, 让服务器绑定该端口.
主要用到的构造方法 :
Socket(String host, int port)
两个参数表示 ip 和 端口号, TCP是有连接的, 在客户端 new Socket 时, 就会尝试和指定 ip 端口的目标建立连接.
主要用到的方法 :
InetAddress getInetAddress()
返回套接字所连接的地址
InputStream getInputStream()
返回此套接字的输入流
OutputStream getOutputStream()
返回此套接字的输出流
void close()
关闭此套接字
TCP 是面向字节流的, 所以我们可以通过上述字节流对象进行数据传输.
从 InputStream 读数据, 就相当于从网卡接收.
往 OutputStream 写数据, 就相当于从网卡发送.
服务器
public class TcpEchoServer {
// serverSocket 就是外场拉客的小哥哥
// serverSocket 只有一个.
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
// 服务器不止是对一个客户端进行服务
// 加上 while 循环, 是让服务器随时可以获取到新的客户端的请求连接
while(true) {
// clientSocket 就是内场服务的小姐姐.
// clientSocket 会给每个客户端都分配一个.(可以通过它来获取客户端的信息)
Socket clientSocket = serverSocket.accept();
// 我们对多客户端的交互应该是并行的
// 所以创建线程来对每个客户端进行响应
Thread t = new Thread(() -> {
try {
// 该方法就是对客户端进行交互了
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
}
}
private void processConnection(Socket clientSocket) throws IOException {
// 提醒客户端已上线.
System.out.printf("[%s : %d] 客户端已上线!\n",
clientSocket.getInetAddress(),clientSocket.getPort());
// 将输入,输出流放入 try() 中, 可以不用考虑资源释放.
// 通过 clientSocket 获得输入, 输出流.
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
// 因为 tcp 是面向字节流的, 所以为了方便读取, 给输入流套一个外壳
// Scanner 是面向字符流.
Scanner scanner = new Scanner(inputStream);
// 同样的, 为了方便写入, 给输出流套一个壳
// PrintWriter 是面向字符流
PrintWriter printWriter = new PrintWriter(outputStream);
// 因为服务器与客户端交互不是一次就完了, 所以得加个循环
while(true) {
// 判断是否还有数据在输入流中,如果没有了就认为客户端下线了,退出循环
if(!scanner.hasNext()) {
System.out.printf("[%s : %d] 客户端已下线!\n",
clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
// 通过 scanner 读取网卡(客户端的请求)
String request = scanner.next();
// 通过请求计算响应
String response = process(request);
// 将响应写入缓存区, 当缓存区满了 就会写入网卡
// 因为 Scanner 读取是不会读空格和回车的
// 所以写入的时候要加换行, 以便客户端区分请求和响应
printWriter.println(response);
// 因为写入缓存区时, 缓存区并不一定会刷新
// 所以我们要手动刷新, 将缓存中的数据刷入网卡
printWriter.flush();
System.out.printf("[%s %d] req: %s resp: %s\n", clientSocket.getInetAddress(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
// 因为 clientSocket 在客户端下线后就没用了,所以要释放
// serverSocket 之所以不要释放, 是因为它的生命周期伴随整个程序
// 程序结束, 它自然被释放了
clientSocket.close();
}
}
//计算响应
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(8080);
tcpEchoServer.start();
}
}
客户端
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIP, int port) throws IOException {
// 这个操作相当于让客户端和服务器建立 tcp 连接.
// 这里的连接连上了, 服务器的 accept 就会返回.
socket = new Socket(serverIP,port);
}
public void start() {
// 这个 scanner 是用来读取用户输入内容的
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter printWriter = new PrintWriter(outputStream);
// 这里的 scannerFromSocket 是用来读取网卡(服务器响应)
Scanner scannerFromSocket = new Scanner(inputStream);
// 通过循环来对服务器进行多次请求
while(true) {
// 提示用户输入
System.out.printf("-> ");
// 读取键盘输入
String request = scanner.next();
// 将键盘输入写入缓存, 当缓存满了就自动刷新 并写入网卡
// 注意带回车, 不然服务器读不到空白符,会一直读
printWriter.println(request);
// 手动刷新缓存, 将数据写入网卡
printWriter.flush();
// 读取服务器写入网卡的响应
String response = scannerFromSocket.next();
System.out.printf("req: %s resp: %s\n", request,response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 8080);
tcpEchoClient.start();
}
}
服务端 :
客户端 :
当然了, 我们还可以多启动几个客户端, 与服务器进行交互.
服务端 :
对服务器进行改进(使用线程池)
这里主要是修改 start 方法里的线程创建, 就单独拿出来修改 :
public void start() throws IOException {
while(true) {
ExecutorService executorService = Executors.newCachedThreadPool();
Socket clientSocket = serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
效果还是一样的.
TCP 版本的字典客户端和字典服务器
其实和 UDP 版的基本一样, 只需要对服务端进行修改 :
public class TcpDictServer extends TcpEchoServer{
private HashMap<String,String> dict = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
dict.put("pen","笔");
dict.put("dog","狗");
dict.put("cat","猫");
}
public String process(String request) {
return dict.getOrDefault(request,"未找到该单词");
}
public static void main(String[] args) throws IOException {
TcpDictServer tcpDictServer = new TcpDictServer(8080);
tcpDictServer.start();
}
}