TCP协议与UDP协议
目录
一、TCP与UDP的特点
(一)有连接VS无连接
(二)可靠传输VS不可靠传输
(三)面向字节流VS面向数据报
(四)全双工VS半双工
二、UDP协议中的socket api
(一)DatagramSocket类
(二)DatagramPacket类
(三)InetSocketAddress类
三、UDP协议的回显服务器
(一)UdpEchoServer回显服务器
(二)UdpEchoClient客户端
(三)拓展:英译汉服务器
四、TCP协议中的socket api
(一)ServerSocket类
(二)Socket类
五、TCP协议的回显服务器
(一)TcpEchoServer回显服务器
(二)TcpEchoClient客户端
一、TCP与UDP的特点
TCP的特点:
- 有连接
- 可靠传输
- 面向字节流
- 全双工
UDP的特点:
- 无连接
- 不可靠传输
- 面向数据报
- 全双工
(一)有连接VS无连接
这是抽象的概念,指的是虚拟的/逻辑上的连接。
- 对于TCP来说,TCP协议中,就保存了对端的信息: A和B通信,A和B先建立连接,让A保存B的信息,B保存A的信息(彼此之间知道要连接的是哪个)。
- 对于UDP来说,UDP协议本身,不保存对方的信息,就是无连接。
(二)可靠传输VS不可靠传输
在网络上,数据是非常容易出现丢失的情况的(丢包),光信号/电信号都可能受到外界的干扰。
在进行通信时,不能指望一个数据包100%地到达对方。
- 可靠传输指的是,虽然不能保证数据包100%到达,但是能尽可能提高传输成功的概率。
- 不可靠传输只是把数据发了,就不管了。
(三)面向字节流VS面向数据报
- 面向字节流指的是在读写数据时,以字节为单位。
- 面向数据报指的是读写数据时,以数据报为单位。
(四)全双工VS半双工
- 全双工指的是 一个通信链路中,支持双向通信(能读也能写)。
- 半双工指的是 一个通信链路中,只支持单向通信(要么读,要么写)。
二、UDP协议中的socket api
计算机中的“文件”,是一个广义的概念,文件还能代指一些硬件设备(操作系统管理硬件设备,也是抽象成文件,统一管理的)。
UDP协议是用来操作网卡的,将网卡抽象成socket文件,操作网卡的时候,流程和操作普通文件差不多。
(一)DatagramSocket类
DatagramSocket类是用来操作socket文件,发送和接收数据报的。
构造方法:
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到主机的任意一个随机端口号(一般用于客户端)。 |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到主机的一个指定的端口号(一般用于服务端)。 |
成员方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接受到数据报,该方法会阻塞等待)。 |
void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送)。 |
void close() | 关闭此数据报套接字。 |
(二)DatagramPacket类
DatagramPacket就是UDP发送和接收的数据报。
构造方法:
方法签名 | 方法说明 |
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() | 获取数据报中的数据。 |
(三)InetSocketAddress类
构造UDP发送的数据报时,需要传入SocketAddress(父类),该对象可以使用InetSocketAddress(子类)来创建。
InetSocketAddress的构造方法:
方法签名 | 方法说明 |
InetSocketAddress(InetAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号 |
三、UDP协议的回显服务器
Java数据报套接字通信模型:
(一)UdpEchoServer回显服务器
package NetWork;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;//UDP协议的回显服务器
//服务器端
public class UdpEchoServer {private DatagramSocket socket=null;//指定了一个固定端口号, 让服务器来使用.public UdpEchoServer(int port) throws SocketException {socket=new DatagramSocket(port);}//启动服务器public void start() throws IOException {System.out.println("服务器启动");while(true){//循环一次,就相当于处理一次请求。//1.读取请求并解析//创建请求数据报DatagramPacket RequestPacket=new DatagramPacket(new byte[4096],4096);//开始接收,并更新数据报socket.receive(RequestPacket);//2.根据请求, 计算响应. (服务器最关键的逻辑)//把读取到的二进制数据, 转成字符串. 只是构造有效的部分.String request=new String(RequestPacket.getData(),0, RequestPacket.getLength());String response=process(request);//3.把响应返回给客户端//根据 response 构造 DatagramPacket, 发送给客户端.//此处不能使用 response.length(),因为这是String的长度而不是byte数组的长度DatagramPacket ResponsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,RequestPacket.getSocketAddress());//发送构建好的数据报socket.send(ResponsePacket);//4.打印日志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 server=new UdpEchoServer(9090);server.start();}}
(二)UdpEchoClient客户端
package NetWork;import java.io.IOException;
import java.net.*;
import java.util.Scanner;//UDP协议的回显服务器
//客户端
public class UdpEchoClient {DatagramSocket socket=null;// 客户端要给服务器发送数据报,首先得知道服务器的IP和端口号private String ServerIp;//目的IPprivate int ServerPort;//目的端口号// 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.ServerIp = serverIp;this.ServerPort = serverPort;socket = new DatagramSocket();}public void start() throws IOException {Scanner sc=new Scanner(System.in);while(true){// 1.读取用户输入的内容System.out.println("请输入要发送的内容:");if(!sc.hasNext()){break;}String request=sc.next();// 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.// 构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号DatagramPacket RequestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(ServerIp),ServerPort);// 3. 发送数据报socket.send(RequestPacket);// 4. 接收服务器的响应DatagramPacket ResponsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(ResponsePacket);// 5. 从服务器读取的数据进行解析, 打印出来.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();}
}
(三)拓展:英译汉服务器
当我们需要实现另外一个简单的服务器时,例如英译汉服务器,只需要继承然后重写process方法就可以了。
package NetWork;import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;//英译汉服务器
public class UdpDictServer extends UdpEchoServer{private HashMap<String,String> dict=new HashMap<>();//要在子类的构造方法中调用父类的构造方法//构造方法初始化字典public UdpDictServer(int port) throws SocketException {super(port);dict.put("apple","苹果");dict.put("boy","男孩");dict.put("cat","小猫");dict.put("dog","小狗");}//重写process方法public String process(String request){return dict.getOrDefault(request,"没有找到该词汇");}public static void main(String[] args) throws IOException {UdpDictServer DictServer=new UdpDictServer(9090);DictServer.start();}
}
四、TCP协议中的socket api
(一)ServerSocket类
ServerSocket是创建TCP服务器端Socket的API。
构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务器端套接字Socket,并绑定到指定端口。 |
成员方法:
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务器端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待。 |
void close() | 关闭此套接字 |
(二)Socket类
Socket类是客户端socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务器端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
构造方法:
方法签名 | 方法说明 |
Socket(String host,int port) | 创建一个客户端套接字Socket,并对应IP的主机上对应端口的进程进行连接。 |
成员方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutPutStream() | 返回此套接字的输出流 |
五、TCP协议的回显服务器
(一)TcpEchoServer回显服务器
package NetWork;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 TcpEchoServer {private ServerSocket serverSocket=null;public TcpEchoServer(int port) throws IOException {serverSocket=new ServerSocket(port);}public void start() throws IOException {System.out.println("启动服务器");// 这种情况一般不会使用 fixedThreadPool, 意味着同时处理的客户端连接数目就固定了.ExecutorService executorService = Executors.newCachedThreadPool();while (true) {// tcp 来说, 需要先处理客户端发来的连接.// 通过读写 clientSocket, 和客户端进行通信.// 如果没有客户端发起连接, 此时 accept 就会阻塞.// 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.Socket clientSocket = serverSocket.accept();// 使用多线程的方式来调整// Thread t = new Thread(() -> {// processConnection(clientSocket);// });// t.start();// 使用线程池来调整executorService.submit(() -> {processConnection(clientSocket);});}}private void processConnection(Socket clientSocket){//对clientSocket进行读写操作System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());try(InputStream inputStream=clientSocket.getInputStream();OutputStream outputStream=clientSocket.getOutputStream()){// 针对 InputStream 套了一层Scanner scanner = new Scanner(inputStream);// 针对 OutputStream 套了一层PrintWriter writer = new PrintWriter(outputStream);while(true){//因为输入流中的数据是持续读取的,要加上循环// 1. 读取请求并解析. 可以直接 read, 也可以借助 Scanner 来辅助完成.if (!scanner.hasNext()) {//scanner.hasNext():判断输入流中是否还有 “下一个令牌”(默认以空白字符分割,如空格、换行等)。// 连接断开了System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());break;}// 2. 根据请求计算响应String request=scanner.next();String response=process(request);// 3. 返回响应到客户端// outputStream.write(response.getBytes());writer.println(response);//将缓存区中的数据都发送出去,避免残留writer.flush();// 打印日志System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),request, response);}} catch (IOException e) {throw new RuntimeException(e);} finally {try {//服务器连接一个客户端就要创建一个clientSocket,使用完就要关闭.clientSocket.close();} catch (IOException e) {throw new RuntimeException(e);}}}private String process(String request){return request;}public static void main(String[] args) throws IOException {TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);tcpEchoServer.start();}
}
(二)TcpEchoClient客户端
package NetWork;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 {// 直接把字符串的 IP 地址, 设置进来.// 127.0.0.1 这种字符串socket = new Socket(serverIp, serverPort);}public void start()throws IOException{Scanner scanner=new Scanner(System.in);try(InputStream inputStream=socket.getInputStream();OutputStream outputStream=socket.getOutputStream()){//给inPutStream套一层Scanner scannerNet= new Scanner(inputStream);//给outPutStream套一层PrintWriter writer=new PrintWriter(outputStream);while (true){//1.读取用户输入String request=scanner.next();//2.发送请求并刷新缓存区数据writer.println(request);writer.flush();//3.接收服务器的响应String response=scannerNet.next();//4.打印出响应System.out.println(response);}}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);client.start();}
}
注意点:
- 在服务器中,采用多线程的方式来处理客户端的请求(使用线程池)。因为如果是单线程有多个客户端连接,当程序处理processConnection请求时,就可能阻塞在processConnection,而不能accpet。
- 因为服务器中有scanner.hasNext来判断发来的请求,所以客户端发送的请求要以换行符/空白符号结束,因此发送时用writer.println。
- 发送请求后记得使用flush刷新缓冲区的数据。