这样理解比较难以理解:我们做个比喻。 TCP好比是打电话,UDP好比是发短信: 打电话时候必须双方确认,才能进行通话,发短信时候是不用接收方同意的,直接发送就行了。所有TCP是有连接,UDP是无连接的。为什么TCP是可靠传输,UDP是不可靠传输,因为打电话时候,必须双方确认才能进行通信,也保证了对方接收到了你的信息,发短信时候,我们并不会知道对方是否收到这条消息,打电话时候是你说一句,我说一句,可以一条一条来说,这就类似于面向字节流,发短信是一次编辑完整个要说的内容,然后发送类似于面向数据包,打电话是不限制我们时间的,而发短信会限制我们的字数多少。
一般使用在一些数据不是很重要的场景,可以丢失一些数据,举个不是很恰当的例子:比如共享单车的定位数据, 其实比较重要的是起始位置的日志数据和终点的日志数据,关于骑行中的日志数据,可能就显得不那么重要,这种情况其实可以使用UDP。像微信聊天这种,每条数据都很重要就不能使用UDP。
定义接收端和服务端的: DatagramSocket的构造方法:
普通方法:接收数据和发送数据
DatagramPacket(数据报) DatagramPacket是UDP Socket发送和接收的数据报 类似于打包数据的东西: 构造方法:
普通方法:
主要是用于记录客户端的地址和端口,为服务器返回数据提供客户端地址。
接收端实现:
package network;/*
*@ 代阳敲的专属代码
**/
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class UdpEchoServer {
//定义个DatagramSocket的引用
private DatagramSocket socket;
public UdpEchoServer(int port) throws SocketException {
//创建一个服务器,port指定端口
this.socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务端已经启动");
//循环24小时处理客户端消息
while (true) {
//定义打包数据
DatagramPacket requestpacket=new DatagramPacket(new byte[1024],1024);
//接收客户端数据
socket.receive(requestpacket);
//转化为String类型,这个只适合文本数据,不可以是图片数据,图片数据转化为二进制。
String request=new String(requestpacket.getData(),0,
requestpacket.getLength(),"UTF-8");
//处理数据,并把反馈数据赋给response
String response=process(request);
//重新打包数据
DatagramPacket responedPacket =new DatagramPacket(response.getBytes(StandardCharsets.UTF_8),
0, response.getBytes().length,requestpacket.getSocketAddress());
//发送给客户端
socket.send(responedPacket);
System.out.printf("[%s:%d] req: %s,resp: %s\n",requestpacket.getAddress().toString(),requestpacket.getPort(),request,response);
}
}
//具体处理客户端数据的逻辑
protected String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(8080);
udpEchoServer.start();
// UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",8080);
// udpEchoClient.strat();
}
}
客户端实现:
package network;/*
*@ 代阳敲的专属代码
**/
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket;
//客户端要发给那个服务端的IP
private String sererIp;
//客户端要发给那个服务端的端口
private int serverPort;
//创建客户端,并指定要发那个服务端的IP和端口
public UdpEchoClient( String sererIp, int serverPort) throws SocketException {
this.sererIp = sererIp;
this.serverPort = serverPort;
this.socket=new DatagramSocket();
}
public void strat() throws IOException {
Scanner scanner =new Scanner(System.in);
System.out.println("客户端启动");
while(true) {
System.out.println("->");
String request=scanner.next();
//打包客户端要发送的数据,参数(转化数据,从那开始,结束长度,服务端的地址)
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),0,request.getBytes().length,
new InetSocketAddress(sererIp,serverPort));
//发送
socket.send(requestPacket);
//打包接收服务端的传输的数据
DatagramPacket responsePacket =new DatagramPacket(new byte[1024],1024);
//接收服务端的数据
socket.receive(responsePacket);
String response =new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
// UdpEchoServer udpEchoServer=new UdpEchoServer(8080);
// udpEchoServer.start();
UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",8080);
udpEchoClient.strat();
}
}
效果:
其实基本逻辑是一样的,我们只需要改造返回逻辑就行了。
package network;/*
*@ 代阳敲的专属代码
**/
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer extends UdpEchoServer{
private Map<String,String> dict =new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat", "⼩猫");
dict.put("dog", "⼩狗");
dict.put("fuck", "卧槽");
}
//修改处理数据逻辑
@Override
public String process(String request) {
return dict.getOrDefault(request,"该词没有查询到");
}
public static void main(String[] args) throws IOException {
UdpDictServer server=new UdpDictServer(9090);
server.start();
}
}
效果:
借助网上一张图再次讲述逻辑:哈哈哈,我感觉他讲的真的很详细
微信这种重要数据就必须全部保证传输到位,不能有一点丢失,而且必须建立连接
服务端
package network;/*
*@ 代阳敲的专属代码
**/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoService {
private ServerSocket socket;
// 这个操作就会绑定端⼝号
public TcpEchoService(int port) throws IOException {
if(1024>port||port>65535) {
throw new BindException("端口不合法");
}
this.socket=new ServerSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while(true) {
Socket clienSocket =socket.accept();
processConnection(clienSocket);
}
}
// 通过这个⽅法来处理⼀个连接的逻辑
private void processConnection(Socket clienSocket) {
System.out.printf("[%s:%d] 客户端上线!\n",clienSocket.getInetAddress().toString(),clienSocket.getPort());
try {
InputStream inputStream=clienSocket.getInputStream();
OutputStream outputStream=clienSocket.getOutputStream();
// ⼀次连接中, 可能会涉及到多次请求/响应
while(true) {
Scanner scanner=new Scanner(inputStream);
if(!scanner.hasNext()) {
// 读取完毕, 客⼾端下线
System.out.printf("[%s:%d] 客⼾端下线!\n" ,clienSocket.getInetAddress(),clienSocket.getPort());
break;
}
//
String request=scanner.next();
// 2. 根据请求计算响应
String response = process(request);
//把数据返回给客户端,用Prwinter包裹一层
PrintWriter writer=new PrintWriter(outputStream);
writer.println(response);
writer.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n",
clienSocket.getInetAddress(),clienSocket.getPort(),request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
// 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
try {
clienSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoService server =new TcpEchoService(8080);
server.start();
}
}
客户端
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;
public class TcpEchoClient {
Socket socket;
//和服务器通信,就要知道服务器的ip和端口
public TcpEchoClient(String serverIp,int port) throws IOException {
socket=new Socket(serverIp,port);
}
public void start() {
System.out.println("客户端启动");
Scanner scannerConsle =new Scanner(System.in);
try {
InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream();
while(true) {
//从控制台输入字符
System.out.println("->");
String request=scannerConsle.next();
//把请求发送给服务端
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
//刷新缓存区
printWriter.flush();
//从服务端返回数据
Scanner sannerNetwork =new Scanner(inputStream);
String response= sannerNetwork.next();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client =new TcpEchoClient("127.0.0.1",8080);
client.start();
}
}
实现的效果和UDP是类似的。
我们多对一的时候发现,其中一个客户端没有得到服务器的响应这是为什么莫呢? 原因如下:
这是因为我们第一个客户端建立连接后,进入process方法后,一直处于循环状态,导致第二个客户端没办法让和服务端建立连接。
我们可以用多线程和线程池解决这个问题。
多线程:
线程池:
我们会发现,UDP是不显示服务端和客户端连接的信息,而TCP是会显示的。所以这也证明了,UDP是不连接的,TCP是连接的。
问题:假设现在有100万个客户同时访问服务器,与服务器建立连接,那么服务器就要创建100万个线程,那么就会造成服务器资源耗尽,最终崩溃,面对这种情况我们如果解决?
以我现在了解的知识是用线程池解决。
我们首先了解一下,线程池的原理:
线程池有几个重要参数,核心线程,最大线程,阻塞队列,当我们创建的线程数达到核心线程数,就会再次创建的线程数放在阻塞队列里面,然后当阻塞队列也满了的话,就会创建临时线程,当达到最大线程数时候,再有客户端访问,就会直接拒绝掉,这也保证了服务器不会无限制创建线程,导致资源耗尽服务器崩溃。
TCP发送数据时候,都需要建立连接,什么时候关闭决定着是长连接或者短连接
短连接:建立连接后,客户端发送一次消息后,并接收到服务器的响应后,必须关闭的连接就是短连接。 长连接:建立连接后,客户端发送一次消息后,并接收到服务器的响应后,不需要关闭连接,保持长时间连接的就是长连接。
场景:短连接一般是客户端发送给服务端请求,类似于访问浏览器。客服会话。 长连接一般是客户端请求服务端,也可以是服务端给客户端发送请求。比如聊天室,实施游戏。
扩展:BIO(同步阻塞IO)和NIO(同步非阻塞IO) 基于BIO的长连接,会一直占用系统资源,一直会占用一个线程,阻塞等待接收消息,对于高并发场景,这样会消耗巨大的资源,服务器是撑不住的,比如一百万个人同时基于BIO的长连接,那么就要有一百万个线程被创建,持续阻塞。
NIO是用一个线程处理多个连接,这样就提高效率,也缓解了服务器的压力。