赞
踩
【网络编程】网络编程中的基本概念及Java实现UDP、TCP客户端服务器程序(万字博文)
【网络原理】UDP协议的报文结构 及 校验和字段的错误检测机制(CRC算法、MD5算法)
文章目录
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称网络数据传输)。
即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。但是,我们的目的是提供网络上的不同主机,基于网络来传输数据资源。
网络编程,本质上就是学习“传输层”给“应用层”提供的API,通过代码的形式,把数据交给传输层,进一步的通过层层封装,就可以把数据通过网卡发送出去了。
客户端:主动发起通信的一方,称为客户端.
服务器:被动接受的一方,称为服务器,可以提供对外服务.
同一个程序在不同场景中,可能是客户端也可能是服务器(服务器可能还需要主动向别的服务器发起通信,此时的服务器相对于被发起通信的服务器来说,就是客户端).
请求(request):客户端给服务器发送数据。
响应(response):服务器给客户端返回数据。
一般来说,获取一个网络资源,涉及到两次网络数据传输:
就比如在快餐店点一份炒饭:
先要发起请求:点一份炒饭;再有快餐店提供的对于响应:提供一份炒饭。
前面说过,要想进行网络编程,需要使用的系统API,本质上是由传输层提供的。
传输层涉及到的主要协议有两个:
TCP的特点:
UDP的特点:
UDP socket API的使用
DatagramSocket 是 UDP Socket(套接字),用于发送和接收UDP数据报
构造方法:
重要方法:
DatagramPacket 是 UDP Socket(套接字)发送和接收的数据报(每次发送接收数据的基本单位)
构造方法:
通过这个程序,了解 socket api 的使用,和典型的客户端服务器基本工作流程。
对于服务器,需要指定端口号来创建 socket (类似于饭店,需要指定具体位置),主要流程如下:
注意事项详见代码注释:
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.SocketException;
-
- //Udp回显服务器
- public class UdpEchoServer {
-
- //Udp套接字
- private DatagramSocket socket;
-
- public UdpEchoServer(int port) throws SocketException {
- socket = new DatagramSocket(port); //服务器:指定端口号创建
- }
-
- public void start() throws IOException {
- System.out.println("服务器启动");
- while (true) {
- //1.接收客户端的请求,并解析
- DatagramPacket requestServer = new DatagramPacket(new byte[4096], 4096);
- socket.receive(requestServer);
-
- //2.根据请求,计算出响应
- String request = new String(requestServer.getData(), 0, requestServer.getLength());
- String response = process(request);
-
- //3.将响应写回给客户端(需要指定发送到的IP地址及端口号)
- DatagramPacket responseServer = new DatagramPacket(
- response.getBytes(), response.getBytes().length, requestServer.getSocketAddress());
- socket.send(responseServer);
-
- //打印日志
- System.out.printf("[%s:%d] request:%s response:%s\n",
- responseServer.getAddress(), responseServer.getPort(), request, response);
- }
- }
-
- //根据请求计算响应(由于是回显程序,直接返回请求的内容)
- public String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
- udpEchoServer.start();
- }
- }
对于客户端,服务器的端口号可以由系统随机分配,但需要知道服务器的IP地址及端口号(去饭店吃饭,需要知道饭店的地址及具体是哪个门店),主要流程如下:
- import java.io.IOException;
- import java.net.*;
- import java.util.Scanner;
-
- //Udp回显客户端
- public class UdpEchoClient {
-
- private DatagramSocket socket;
- private String address;
- private int port;
-
- //客户端需要知道服务器的IP地址及端口号
- public UdpEchoClient(String address, int port) throws SocketException {
- this.address = address;
- this.port = port;
- socket = new DatagramSocket(); //服务器:随机端口号创建
- }
-
- public void start() throws IOException {
- System.out.println("客户端启动");
- Scanner in = new Scanner(System.in);
- while (true) {
- System.out.print("-> ");
- if (!in.hasNext()) {
- break;
- }
- //1.控制台读取请求内容
- String request = in.next();
-
- //2.构造请求的数据报,并发送到服务器(需要指定目的IP地址和目的端口号发送请求)
- DatagramPacket requestPacket = new DatagramPacket(
- request.getBytes(), request.getBytes().length, InetAddress.getByName(address), port);
- socket.send(requestPacket);
-
- //3.读取服务器的响应,并解析出响应的内容
- DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
- socket.receive(responsePacket);
- String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
-
- //4.将响应内容输出到控制台
- System.out.println(response);
- }
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
- udpEchoClient.start();
- }
- }
先运行服务器,再运行客户端,看程序的执行效果:
这个程序并不能直接做到“跨主机通信”,因为这台主机可能不能直接访问到另一台主机(NAT机制)。但是可以通过以下手段实现“跨主机通信”:
经过这样的操作,其他主机通过运行上述的客户端程序,就能够发起通信了。
基于上述回显服务器,还可以实现出一些其他带有一点业务逻辑的服务器。
改进成一个“字典服务器”,英译汉的效果。请求是一个英文单词,响应返回对应的中文翻译。
主要逻辑其实和回显服务器基本一致,唯一的区别就在于,服务器端将客户端请求的数据,计算成响应的方式不一致。回显服务器是直接返回客户端请求的数据,这里的字典服务器则是英译汉效果。
而上述代码中,这个根据请求数据计算响应数据的操作,是通过process方法实现的。因此只需要让这个字典服务器继承回显服务器,并重写process方法即可。这里英译汉的业务逻辑通过打表的方式实现。
- import java.io.IOException;
- import java.net.SocketException;
- import java.util.HashMap;
- import java.util.Map;
-
- //Udp字典服务器
- public class UdpDictServer extends UdpEchoServer {
-
- Map<String, String> map;
-
- public UdpDictServer(int port) throws SocketException {
- super(port);
- map = new HashMap<>();
-
- map.put("cat", "小猫");
- map.put("dog", "小狗");
- map.put("animal", "动物");
- }
-
- //通过重写 计算响应的process方法,达成 英->汉 的效果
- @Override
- public String process(String request) {
- return map.getOrDefault(request, "找不到该单词");
- }
-
- public static void main(String[] args) throws IOException {
- UdpDictServer udpDictServer = new UdpDictServer(9090);
- udpDictServer.start();
- }
- }
先运行字典服务器,再运行回显客户端(这里客户端是通用的,因为回显客户端只进行发送请求和接收响应并解析的操作),看程序的执行效果:
ServerSocket 类是创建TCP服务器端Socket的API. (只能给服务器端使用)
构造方法:
重要方法:
Socket 类用于创建客户端 Socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket. (服务器端和客户端都能使用)
构造方法:
重要方法:
使用TCP协议实现回显客户端服务器程序。与UDP协议实现的最大区别是,TCP是有连接的,和打电话一样,需要一方(客户端)拨号,一方(服务器)接通,因此TCP协议首要操作就是等待客户端连接。
和UDP回显服务器一样,对于这里的服务器,同样需要指定端口号创建TCP服务器端Socket,即ServerSocket。
后续流程和UCP回显服务器一致。此处由于每有一个客户端连接,就会有一个clientSocket,这里消耗的Socket会越来越多,因此每当一个客户端连接结束,就需要释放这个clientSocket。
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.io.PrintWriter;
- import java.net.ServerSocket;
- import java.net.Socket;
- import java.util.Scanner;
-
- //TCP回显服务器
- public class TcpEchoServer {
-
- private ServerSocket serverSocket;
-
- public TcpEchoServer(int port) throws IOException {
- //指定服务器端口号,创建一个serverSocket
- serverSocket = new ServerSocket(port);
- }
-
- public void start() throws IOException {
- System.out.println("服务器启动!");
- while (true) {
- //监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
- Socket clientSocket = serverSocket.accept();
- processConnection(clientSocket);
- }
- }
-
- private void processConnection(Socket clientSocket) throws IOException {
- System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
- try (InputStream inputStream = clientSocket.getInputStream();
- OutputStream outputStream = clientSocket.getOutputStream()) {
- while (true) {
- //1.读取客户端请求的数据
- //利用scanner读取客户端输入的信息
- Scanner scanner = new Scanner(inputStream);
- if (!scanner.hasNext()) {
- System.out.printf("[%s:%d] 客户端下线\n",
- clientSocket.getInetAddress(), clientSocket.getPort());
- break;
- }
- //这里的next()需要遇到\n才停止,因此需要对端写入的时候,要同时写入\n换行符
- String request = scanner.next();
-
- //2.解析请求的数据,并计算出响应
- String response = process(request);
-
- //3.将响应写回到客户端
- //outputStream.write(response.getBytes(), 0, response.getBytes().length);
- PrintWriter writer = new PrintWriter(outputStream);
- writer.println(response);
- writer.flush();
-
- //打印日志
- System.out.printf("[%s:%d] request:%s response:%s\n",
- clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
- }
-
- } catch (IOException e) {
- throw new RuntimeException(e);
- } finally {
- clientSocket.close();
- }
- }
-
- //回显服务器,直接返回原数据
- public String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
- tcpEchoServer.start();
- }
- }
对于客户端,需要指定服务器的IP和端口号建立连接。使用 Socket(String host, int port) 创建Socket的时候,就开始发起与对应服务器建立连接的请求了。
主要流程和UDP回显客户端程序的流程也基本一致,只需要注意请求和响应数据的方式是不同的,是通过操作输入输出流完成的即可。
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.io.PrintWriter;
- import java.net.InetAddress;
- import java.net.Socket;
- import java.util.Scanner;
-
- //TCP回显客户端
- public class TcpEchoClient {
-
- private Socket clientSocket;
-
- //需要指定服务器的IP和端口号
- public TcpEchoClient(String serverAddress, int serverPort) throws IOException {
- //与对应客户端建立连接
- clientSocket = new Socket(InetAddress.getByName(serverAddress), serverPort);
- }
-
- public void start() {
- System.out.println("客户端启动!");
- try (Scanner scannerConsole = new Scanner(System.in);
- InputStream inputStream = clientSocket.getInputStream();
- OutputStream outputStream = clientSocket.getOutputStream()) {
- while (true) {
- //1.用户从控制台输入数据
- System.out.print("-> ");
- String request = scannerConsole.next();
-
- //2.将该数据作为请求,发送给服务器
- //outputStream.write(request.getBytes(), 0, request.getBytes().length);
- //outputStream.write('\n');
- PrintWriter writer = new PrintWriter(outputStream);
- writer.println(request);
- writer.flush(); //刷新缓冲区,确保数据发送出去
-
- //3.读取服务器的响应,并解析响应的内容
- Scanner scannerNetwork = new Scanner(inputStream);
- String response = scannerNetwork.next();
-
- //4.将响应输出到控制台
- System.out.println(response);
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
- tcpEchoClient.start();
- }
- }
先运行服务器,再运行客户端,看执行效果:
可以看到,服务器和客户端都能满足我们的需求,但这里其实还存在一个问题。
当我们开启多个客户端想要进行连接通信时,只有第一个连接到的客户端才能正确通信,其他的客户端是没有反应的。要想某个客户端能正常通信,只有当其他客户端都下线(结束程序),这个客户端才能接收到响应数据。
可以看到,此处的这个客户端并没有正确通信,当另一个客户端下线之后,该客户端此前发送的数据又正常请求并响应了。
分析过程:
第二个客户端之前发的请求为什么能被立即处理?
解决上述问题的核心思路就是使用多线程:
由于这里不涉及多个线程修改共享变量,因此没有线程安全问题,我们只需要改动 start 方法即可。
- //多线程
- public void start() throws IOException {
- System.out.println("服务器启动!");
- while (true) {
- //监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
- Socket clientSocket = serverSocket.accept();
-
- Thread t = new Thread(() -> {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
- t.start();
- }
- }
通过引入多线程,这里的服务器就能支持多个客户端同时与其通信了。
上述问题,不是TCP引起的,而是代码两次循环嵌套引起的,UDP服务器,就是只有一层循环,因此不会有这个问题。
而这个多线程版本同样还有一些问题:
前面讲过,线程池解决的就是线程频繁创建和销毁的问题,因此,这里的优化方案就是使用线程池。
- public void start() throws IOException {
- System.out.println("服务器启动!");
- ExecutorService threadPool = Executors.newCachedThreadPool();
- while (true) {
- //监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
- Socket clientSocket = serverSocket.accept();
-
- threadPool.submit(new Runnable() {
- @Override
- public void run() {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
- }
线程开销问题解决了。但是,如果当前的场景使线程频繁创建,但是不销毁呢?
要解决这个新问题,还可以引入其他的方案:
同UDP字典客户端服务器程序:
- import java.io.IOException;
- import java.util.HashMap;
- import java.util.Map;
-
- //TCP字典服务器
- public class TcpDictServer extends TcpEchoServer {
-
- Map<String, String> map;
-
- public TcpDictServer(int port) throws IOException {
- super(port);
-
- map = new HashMap<>();
- map.put("cat", "小猫");
- map.put("dog", "小狗");
- map.put("animal", "动物");
- }
-
- @Override
- public String process(String request) {
- return map.getOrDefault(request, "未找到该单词");
- }
-
- public static void main(String[] args) throws IOException {
- TcpDictServer tcpDictServer = new TcpDictServer(9090);
- tcpDictServer.start();
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。