目录
- 前言
- UDP 版的回显服务器
- 需要用到的 api
- 服务端
- 客户端
- UDP 版本的字典客户端和字典服务器
- TCP 版的回显服务器
- 需要用到的 api
- 服务器
- 客户端
- 对服务器进行改进(使用线程池)
- TCP 版本的字典客户端和字典服务器
前言
我们写网络程序, 主要编写的是应用层代码.
真正要发送这个数据, 还需要上层协议调用下层协议, 也就是应用层调用传输层.
传输层给应用层提供了一组 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); //如果没接收到信息就会阻塞等待// 为了方便我们处理这个请求, 将数据包转为 StringString 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","猫");//...可以无限添加, 其实网上的翻译词典什么的,就是样本比这个大}@Overridepublic 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() {@Overridepublic 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();}}