> 文章列表 > 【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器

【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器

【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器

作者:学Java的冬瓜
博客主页:☀冬瓜的主页🌙
专栏:【JavaEE】
主要内容:传输层协议对应Socket编程,DatagramSocket,DatagramPacket,Udp版本的客户端服务器,UdpEchoSever,UdpEchoClient,Udp版本的查词典服务器底层原理;Tcp版本的客户端和服务器,TcpEchoServer,TcpEchoClient。Tcp版本的服务器的几个要点。

文章目录

  • 一、UDP和TCP
  • 二、Udp版本客户端服务器
    • 1、DatagramSocket和DatagramPacket(数据报)
    • 2、UdpEchoSever&&UdpEchoClient
      • 2.1、什么是Echo Sever?
      • 2.2、UDP客户端+UDP回显服务器代码
      • 2.3、查词典服务器代码
  • 三、Tcp版本客户端服务器
    • 1、ServerSocket和Socket
    • 2、TcpEchoServer&&TcpEchoClient
      • 2.1、Tcp客户端
      • 2.2、Tcp服务器
  • 三、UDP和TCP总结

一、UDP和TCP

       Socket API 是操作系统给应用程序提供的来进行网络数据的 发送和接收的api(即传输层给应用层使用的api)。
在需要通过操作系统来执行的传输层里,提供了两个最核心的协议:UDP和TCP。因此Socket API也提供了两种风格:UDP、TCP。下面我们来看看UDP和TCP两种方式有什么区别。

TCP:有连接,可靠传输,面向字节流,全双工。
UDP:无连接,不可靠传输,面向数据报,全双工。

直接记上面的功能可能有点懵,那我们来举例子来类比理解:
比如关于连接:打电话时,双方是先拨通后,才进行你一句我一句的通话,是有连接的;而如果是发消息,因为不需要拨通等双方都先接受,是无连接的。
比如关于是否可靠传输:打电话时,可以互相回应,可以知道我的消息对方收到没有,这是可靠传输。而如果是发消息,则我无法确定对方是否收到我的消息,则是不可靠传输。
面向的对象 :TCP是面向字节流,即操作单位是字节;而UDP是面向数据报进行编程,即操作单位是数据报(一个数据报带有一定的格式,可能有多个字节)。
全双工:比如一根水管,它只能实现单向输水,可以叫做半双工;而这里的全双工指的是一个通信管道,可以双向传输(既可以发送也可以接收),怎么实现的? 一根网线里,其实有8根线,4根负责传输,4根负责接收,这样就完美实现全双工。

二、Udp版本客户端服务器

1、DatagramSocket和DatagramPacket(数据报)

基于 UDP 来编写一个客户端服务器的网络通信程序:
DatagramSocket:使用这个类表示一个Socket对象,在操作系统中,把这个Socket对象也当成是一个文件来处理,是在文件描述符表上的一项。
区别是:普通文件对应的硬件是 硬盘;而Socket对象对应的硬件是网卡,或者说操作系统内核中,用"Socket"这样的文件对象来抽象表示网卡。
DatagramPacket:表示UDP传输中的一个数据报。

DatagramSocket类的相关方法:
构造方法:

构造方法 说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口一般用于客户端)
DatagramSocket(intport) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
  • 进程关联了端口号,本质上是进程里的Socket对象关联了 端口号。同时一个进程可以创建多个Socket对象,每个Socket对象都可以连接到不同的网络地址和端口。因此一个进程可以关联多个端口,但一个端口只能关联一个进程。

普通方法:

普通方法 说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字
  • receive方法中的DatagramPacket是我们创建的传入的一个空的对象,当receive接收到发送方发来的数据报时,才把发送方发来的内容填充进入我们传入的这个空的DatagramPacket对象,得到接收到的数据报,这个参数也叫做"输出型参数"。

DatagramPacket类(数据报)的相关方法:
构造方法:

构造方法 说明
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() 获取数据报中的数据(字节数组的形式)
int getLength() 获取数据报中数据的长度
SocketAddress getSocketAddress() 获取数据报发送端主机的IP和端口号(DatagramPacket中隐含发送方的IP和端口)

2、UdpEchoSever&&UdpEchoClient

2.1、什么是Echo Sever?

Echo Sever是一种基于客户端/服务器模型的网络应用程序,它的功能是将客户端发送的数据原封不断地返回给客户端,称为回显服务器。
这里我们主要来理解怎么实现客户端服务器,以及Socket API的使用,所以就省略了中间的业务逻辑,而是直接把客户端发来的数据直接返回。

2.2、UDP客户端+UDP回显服务器代码

客户端

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;public class UdpEchoClient {private DatagramSocket socket = null;// 客户端的ip是环回ip(127.0.0.1),端口是操作系统随机分配的一个端口// 因为在本机模拟通信,所以服务器的ip也是环回ip(127.0.0.1),端口是程序员指定的// 服务器的ip和端口都得告诉客户端,我们才能在客户端访问服务器private String serverIp = null;private int serverPort = 0;public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.socket = new DatagramSocket();this.serverIp = serverIp;this.serverPort = serverPort;}public void start() throws IOException {System.out.println("客户端启动!");Scanner scanner = new Scanner(System.in);while (true){// 1.从控制台读取数据到一个空的DatagramPacket中System.out.print("> ");String request = scanner.next();if(request.equals("exit")){System.out.println("客户端关闭!");break;}// 注意1:InetAddress.getByName(serverIp)操作把点分十进制的ip(127.0.0.1)转换成32位二进制数// 注意2:发送数据报时,使用String的getBytes().length方法获取数据报长度DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp), this.serverPort);// 2.把DatagramPacket发给服务器socket.send(requestPacket);// 3.使用空的DatagramPacket,接收服务器处理后的响应数据DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);socket.receive(responsePacket);  // 注意:如果receive没有接收到响应数据,那就会阻塞等待。// 4.打印响应结果// 注意1:打印返回的响应结果,不能用toString,因为你无法为DatagramPacket类重写toString方法// 注意2:接收数据报时使用DatagramPacket的getLength方法获取数据报长度String response = new String(responsePacket.getData(),0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);client.start();}
}

客户端代码步骤:

  1. 用一个空的DatagramPacket类型的 requestPacket接收用户从控制台输入的数据(接收字符串)
  2. 根据给出的服务器的ip和端口,发送这个DatagramPacket类型的 requestPacket给服务器处理数据
  3. 用DatagramPacket类型的空的 responsePacket接收服务器发来的处理后的数据(接收数据报)
  4. 把数据报类型的响应内容 requestPacket转换成字符串 request,便于打印

服务器

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UdpEchoServer {// 注意:1.这个socket对象在操作系统内核中操作时,是当成文件的方式操作,把这个对象当成网卡的抽象private DatagramSocket socket = null;// 注意:2.服务器端需要手动指定一个端口,避免客户端找不到服务器public UdpEchoServer(int port) throws SocketException {this.socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while (true){// 1.给一个空的DatagramPacket,用于接收客户端发来的数据报DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);socket.receive(requestPacket);   // 注意:如果receive没有接收到请求数据,那就会阻塞等待。// 注意1:为了便于处理,把DatagramPacket这个特殊的对象转化成字符串的形式,但是不能用toString,因为你无法为DatagramPacket类重写toString方法// 注意2:接收数据报时使用DatagramPacket的getLength方法获取数据报长度String request = new String(requestPacket.getData(),0,requestPacket.getLength());// 2.对请求内容进行业务处理(这里是回显服务器直接返回)String response = process(request);// 3.构造好响应的DatagramPacket,并把它发回客户端。// (注意1:这里也可以直接使用requestPacket.getSocketAddress()同时获取IP和端口,客户端的端口和ip是requestPacket自带的。//  注意2:第二个参数必须是字节数组长度response.getBytes().length,而不是字符串的长度//        使用String的getBytes().length方法获取数据报长度)DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getAddress(),requestPacket.getPort());socket.send(responsePacket);// 4.为了观察,打印一下客户端发来的的信息System.out.printf("[%s,%d] req:%s; resp:%s\\n",requestPacket.getAddress(),requestPacket.getPort(),request, response);}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer sever = new UdpEchoServer(9090);sever.start();}
}

服务器代码步骤:

  1. 用一个空的DatagramPacket类型的 requestPacket接收 客户端发来的数据(接收数据报)
  2. 把这个requestPacket转换成字符串 request,然后进行业务处理得到字符串 response
  3. 用一个空的DatagramPacket类型的 requestPacket接收响应字符串 response(接收响应字符串)
    然后根据requestPacket自带的客户端的ip和端口,把响应发给客户端。
  4. 打印中间过程,客户端的ip和端口,服务器的处理请求和响应。

执行顺序:

1.服务器先启动,进行到receive进行阻塞,等待客户端发送请求数据报(服务器)
2.客户端读取用户输入内容到请求数据报(客户端)
3.客户端执行send把请求数据报发给服务器(客户端)
4.客户端发送请求数据报后立即执行到receive,等待服务器发来响应数据报(客户端)服务器接收到请求数据报,从服务器的receive阻塞中返回(服务器)
5.服务器根据请求数据报计算响应数据报(服务器)
6.服务器执行send,发送响应数据报给客户端(服务器)
7.客户端从receive阻塞中返回,读到响应数据报(客户端)

2.3、查词典服务器代码

  • 注意:需要复用上面 UDP客户端+服务器 中的代码。
    操作是:使用UdpDictSever继承UdpEchoSever,再重写process方法。
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;public class UdpDictSever extends UdpEchoServer{// 使用一个集合来存放单词集合private Map<String,String> dict = new HashMap<>();public UdpDictSever(int port) throws SocketException {super(port);dict.put("cat","猫");dict.put("beautiful","美丽的");dict.put("perfect","完美的");}@Overridepublic String process(String request){return dict.getOrDefault(request,"没有你要查的单词!");}public static void main(String[] args) throws IOException {UdpDictSever sever = new UdpDictSever(9090);sever.start();}
}

三、Tcp版本客户端服务器

1、ServerSocket和Socket

ServerSocket:专门给服务器使用的Socket对象。
Socket:既可以给客户端使用,也可以给服务器使用的Socket对象。

注意1:
ServerSocket 用于服务器端本身的ServerSocket对象的创建;
Socket 用于客户端本身的Socket对象的创建(指定服务器的ip和端口) 和 服务器端accept与客户端连接后返回的Socket对象

注意2:
服务器端accept后,返回得到一个Socket对象,通过这个Socket对象和客户端 使用字节流进行 发送/接收。

ServerSocket类的相关方法:
构造方法:

构造方法 说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口

普通方法:

普通方法 说明
ServerSocket(int port) 创建一个服务端流套接字ServletSocket类的对象,并绑定到指定端口
Socket accept() 开始监听服务器端的绑定的端口,有客户端连接后,返回给服务器端一个Socket对象,并基于该Socket对象建立与客户端的连接,如果没有客户端连接则accept阻塞等待
void close() 关闭此套接字(Socket)

Socket类的相关方法:
构造方法:

构造方法 说明
Socket(String host, int port) 创建一个客户端流的Socket类的对象,和对应IP的主机上的对应端口建立连接

普通方法:

普通方法 说明
InetAddress getInetAddress() 返回调用该方法的Socket对象的对应连接的Ip
int getPort() 返回调用该方法的Socket对象的对应连接的端口
InputStream getInputStream() 返回调用该方法的Socket对象的输入流
OutputStream getOutputStream() 返回调用该方法的Socket对象的输出流

2、TcpEchoServer&&TcpEchoClient

2.1、Tcp客户端

Tcp版本的客户端和Udp版本的客户端的区别:

Udp版本的客户端(端口和DatagramSocket建立关联) 使用两个成员变量来表示指定服务器的serverIp和serverPort,且发送数据时需要把点分十进制的目标服务器的ip(serverIp)转换成32位的二进制数据,使用数据报进行发送/接收。
Tcp版本的客户端(端口和Socket建立关联) 要先根据指定服务器的serverIp和serverPort建立连接客户端的new Socket时传入serverIp和serverPort,它可以自动识别点分十进制为32位二进制数,然后使用字节流读写网卡(即接收/发送信息),其实是在协议栈里处理,然后交由网卡发送和接收。

客户端:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIp, int serverPort) throws IOException {// 注意1:在客户端new一个Socket对象的时候,就连接服务器。// 注意2:Socket对象可以字节把点分十进制的serverIp转换成32位二进制数socket = new Socket(serverIp,serverPort);}public void start(){System.out.println("客户端启动!");try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){Scanner scanner = new Scanner(System.in);while (true){// 1.客户端从控制台读取用户输入的内容System.out.print(">");String request = scanner.next();if (request.equals("exit")){System.out.println("客户端关闭!");break;}// 2.客户端把请求写入网卡,发送给服务器处理PrintWriter printWriter = new PrintWriter(outputStream);printWriter.println(request);   //注意:要写入"\\n"printWriter.flush();  // 冲刷,保证数据写入网卡// 3.客户端读取服务器响应写回到网卡上的数据Scanner respScan = new Scanner(inputStream);String response = respScan.next();System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);client.start();}
}
客户端代码步骤:
1. 客户端从控制台读取用户输入的内容
2. 客户端把请求写入网卡,发送给服务器处理
3. 客户端读取服务器响应写回到网卡上的数据
4. 打印响应的结果

2.2、Tcp服务器

Tcp版本的服务器和Udp版本的服务器的区别:

Udp版本服务器不需要建立连接,使用数据报传输,如果客户端访问量不是很多,可以不用多线程,可以多个客户端同时访问,直接while就搞定。
Tcp版本需要建立连接,使用字节流传输,使用多线程(或者线程池),如果不用多线程,那么因为每个客户端访问都需要连接,在有客户端连接时,其它客户端则无法连接,导致无法使用,导致效率问题。

Tcp版本的服务器需要注意的点:

  • 1> Tcp版本的服务器需要在发送消息时在数据后面加上\\n。因为接收端读取数据时使用Scanner的next方法读取,next方法规则是:读到换行符/空格/tab时结束,读到的数据不包含以上符号。所以发送端可以在数据的结尾加上\\n,表示读取数据结束。这个点客户端也是一样。如下图:printWriter.println(outputStream)表示在发送数据outputStream后面加上一个\\n。发送outputStream后,一定记得flash,把信息真正的发送。
    【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器
    【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器

  • 2> 在Tcp版本的服务器端中,需要关闭客户端访问时创建的Socket资源。每次有一个客户端访问服务器,就会创建一个Socket对象和客户端的Socket连接。服务器端每创建一个Socket对象,就在服务器的这个进程上的文件描述符表上占用一个空间,而客户端访问量应该是很多的。因此如果连接完成后,不关闭这个Socket,到了文件描述符表位置被占满时,其它客户端就无法再访问服务器了,因此,在每个客户端连接完成后,我们需要关闭服务器端的这个Socket资源,释放这个Socket占用的文件描述符表的位置。
    那么为什么Udp版本的服务器不需要关闭?Udp版本服务器端的的DatagramSocket的生命周期是整个进程。而Tcp版本的clientSocket的生命周期是每个客户端连接时,断开连接,这个Socket就没用了,且因为每创建一个客户端连接,服务器就会创建一个clientSocket,所以数量上也会很多!

  • 3> 短连接和长连接:下列代码的processConnection中的while去掉就是短连接,即传输一次就断开连接,每次访问都得先连接再发送请求;长连接即用while,当一个客户端连接好服务器然后发送请求后,先不断开连接,等待用户再次发送请求,等用户自己退出时才断开连接。
    【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器

  • 4> IO多路复用,如果客户端访问量很大,即使使用多线程服务器压力还是很大,就需要用IO多路复用。比如C10K问题(1w个客户端),C10M问题(1kw个客户端访问)。IO多路复用,可以使用一个线程处理多个客户端的任务。原理:在这个线程中使用一个集合来存放连接对象,这个线程就负责监听这个集合,在集合中哪个连接有数据来了,线程就处理这个连接。在操作系统中提供了select,epoll就可以监听。

服务器:

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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TcpEchoSever {private ServerSocket serverSocket = null;// 注意:服务器本身使用ServerSocket和端口绑定连接public TcpEchoSever(int Port) throws IOException {serverSocket = new ServerSocket(Port);}public void start() throws IOException {System.out.println("启动服务器!");// 注意:使用while保证每次有客户端连接时都能连接到while (true){ 版本一:使用多线程
//            // 注意:每当有一个客户端连接服务器时,创建一个Socket对象和客户端的Socket进行通信
//            Socket clientSocket = serverSocket.accept();
//            // 注意:建立连接使用当前线程,放在我们创建的线程外;使用多线程去处理客户端发来的请求(处理业务)
//            Thread t = new Thread(()->{
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();// 版本二:使用线程池Socket clientSocket = serverSocket.accept();ExecutorService pool = Executors.newCachedThreadPool();pool.submit(new Runnable() {@Overridepublic void run() {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}}// 注意:一个连接对应一个客户端,private void processConnection(Socket clientSocket) throws IOException {// 注意:服务器的每一个Socket对应一个客户端System.out.printf("[%s:%d] 客户端上线!\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){// 注意:由于一个客户端可能要处理多个请求和响应,所以使用循环进行while (true){// 1.服务器读取客户端写入网卡的字节流数据Scanner reqScan = new Scanner(inputStream);if (!reqScan.hasNext()){System.out.printf("[%s:%d] 客户端下线!\\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());break;}String request = reqScan.next();// 注意:next读到换行符/空格/tab结束,但是读取的内容不包含换行符/空格等//    我们这里是从客户端的请求内容就读取,所以客户端发来的请求中应当有以上结束符// 2.对请求进行业务处理String response = process(request);// 3.服务器把响应内容写回网卡,响应给客户端//   操作:用outputStream构造一个PrintWriter字符流对象,便于把"\\n"一并写入网卡PrintWriter printWriter = new PrintWriter(outputStream);printWriter.println(response);printWriter.flush();   // 冲刷,保证数据写入网卡// 4.打印日志System.out.printf("[%s:%d] req:%s; resp:%s\\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}} catch (IOException e) {e.printStackTrace();} finally {clientSocket.close();}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoSever sever = new TcpEchoSever(9090);sever.start();}
}
服务器代码步骤:
1. 服务器读取客户端写入网卡的字节流数据
2. 对请求进行业务处理
3. 服务器把响应内容写回网卡,响应给客户端

执行顺序:

1.服务器先启动,进行到accept进行阻塞,等待客户端new Socket从而建立连接(服务器)
2.客户端从控制台读取用户输入内容(客户端)
3.客户端使用OutputStream把请求发给服务器(客户端)
4.服务器Socket感知到请求并使用InputStream接收请求(服务器)
5.服务器根据请求计算响应(服务器)
6.服务器使用OutputStream把响应发回客户端(服务器)
7.客户端Socket感知到请求并使用InputStream接收请求(客户端)
8.客户端打印响应结果

三、UDP和TCP总结

【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器
【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器