文章目录
- Socket
- 我们来解释一下上面叫做有无连接
- 我们再来理解一下上面是可靠传输和不可靠传输
- 面向字节流与面向数据报的区别(后期会具体分析这个)
- 全双工 单双工
- UDP
- DatagramSocket
- DatagramPacket
- 我们来尝试写一下UDP版本的代码
- TCP
- ServerSocket
- Socket
- 我们来尝试写一下TCP版本的代码
👑作者主页:Java冰激凌
📖专栏链接:JavaEE
Socket
Socket是操作系统给应用程序提供的一组API 可以认为Socket是应用层和传输层之间的桥梁 这就好比是相亲 男方与女方之间的联系是媒婆建立起来的 这个媒婆起到的作用也就是桥梁
传输层中有两类核心协议 TCP和UDP
所以说 在Socket 的API也对应的有两层
TCP:有连接 可靠传输 面向字节流 全双工
UDP:无连接 不可靠传输 面向数据报 全双工
我们来解释一下上面叫做有无连接
有连接:就像我们打电话 当我们拨打电话之后 要等待对方接通电话才可以开始交流
无连接:就像发微信 我们给对方发送了消息之后 我们不关心对方是否看到了这个消息 只能保证我们消息发了(就是发了 也只能是发了 因为我们的网络中难免会有波动 这个消息有可能会丢失的 所以对方可能压根没有收到消息)
我们再来理解一下上面是可靠传输和不可靠传输
可靠传输:我们明确对方当前是否收到了消息 也就是说 还是以发微信为例 当我们发送过消息后 会等待对方给我们回复确认收到消息 如果对方一段时间后没有回复消息 我们会再次重发消息 (这个机制叫做超时重传 后期会讲) 简单总结:传输过程中发送方知道接收方有没有收到消息
不可靠传输:传输过程中 发送方不知道接收方有没有收到消息
此处有一共非常致命的错误理解!!!
可靠传输只能保证数据传输过去对方100%可以接收到 并不能代表可靠传输是安全的传输 并不能保证安全性
面向字节流与面向数据报的区别(后期会具体分析这个)
面向字节流以字节为单位进行传输 (非常类似于文件操作中的字节流)
面向数据报以数据报为单位进行传输(一个数据报都会明确大小 )一次发送/接收必须是一个完整的数据报 不能是半个 也不能是一个半 必须是一个
全双工 单双工
单双工:一条通道单向通信
全双工:一条通道双向通信
我们可以当做是单行路和双行路 一条路只能向南走 不可以往北走 这个就是单双工 一条路南北都可以走 就是全双工
UDP
我们来尝试着使用Java语言编写一共简单的客户端和服务器进行一个简单的交互
首先我们来认识一下UDP socket API
DatagramSocket
创建了一个UDP版本的socket对象 代表着操作系统中的一个socket文件
DatagramPacket
表示了一个UDP数据报 每次发送和接收 都是在传输一个UDP数据报
我们来尝试写一下UDP版本的代码
我们来写一个回显服务器
public class UDPEchoServer {
//我们先来定义一个DatagramSocket类 用来接收数据和发送数据
DatagramSocket datagramSocket = null;
//在构造方法中我们将指定端口号
// 为何要指定端口号?
// 因为我们客户端在发送的时候要明确知道服务器的端口号 所以此处我们手动指定
public UDPEchoServer(int port) throws SocketException {
datagramSocket = new DatagramSocket(port);
}
//启动服务器方法
public void start() throws IOException {
System.out.println("服务器启动");
//因为服务器要不停的接收处理数据 所以我们将它设置为一个无限循环
while(true){
//我们先来创建一个DatagramPacket类用来接收从客户端接收到的数据
DatagramPacket requsePacket = new DatagramPacket(new byte[1024],1024);
//接收客户端发来的数据 当没有接收到数据的时候 这个方法会阻塞等待
datagramSocket.receive(requsePacket);
//我们为了方便处理 将接收到的数据转换为字符串进行处理
String requse = new String(requsePacket.getData(),0,requsePacket.getLength());
//处理数据
String repouse = process(requse);
//打包数据报 其中将要返回的数据 以及端口号都要放入
DatagramPacket repousePacket = new DatagramPacket(repouse.getBytes(), repouse.getBytes().length,
requsePacket.getSocketAddress());
//发送数据报
datagramSocket.send(repousePacket);
//打印查看数据
System.out.printf("[%s : %d] req : %s ; reps : %s\n",requsePacket.getAddress().toString(),
requsePacket.getPort(),requse,repouse);
}
}
//因为我们写的是回显服务器 所以直接返回接收到的字符串即可
private String process (String requse) {
return requse;
}
public static void main (String[] args) throws IOException {
UDPEchoServer udpEchoServer = new UDPEchoServer(8848);
udpEchoServer.start();
}
}
来解读一下这个代码
- 读取客户端发来的请求
- 根据请求计算机响应
- 把响应写回到客户端
- 站在服务器的角度:
源IP:本机的IP 源端口:指定的端口(服务器绑定的端口号) 目的IP:包含在 收到的数据报中 目的端口:包含在收到的数据报中协议类型:UDP
我们来解答一个问题 为啥服务器上来就接收 而不是发送呢?
因为服务器的定义 就是被动接收请求的这一方 主动发起请求的这一方叫做客户端
那么我们怎么来验证这个代码呢?此时我们还需要来完成一个客户端 用来向服务器发送数据并且获取到服务器返回的数据之后再打印到屏幕上
public class UDPEchoClient {
private DatagramSocket datagramSocket = null;
private String IP;//目的IP
private int port;//目的端口
public UDPEchoClient(String IP,int port) throws SocketException {
//此处我们不分配端口号 为了我们的客户端程序可以主动分配一个可用的端口号
datagramSocket = new DatagramSocket();
this.IP = IP;
this.port = port;
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
while(true){
//提醒客户端输入
System.out.printf("-->");
//获取客户端的输入
String requse = scanner.nextLine();
//打包为UDP数据报 其中要放入要传入的目的端口 目的IP
DatagramPacket requsePacket = new DatagramPacket(requse.getBytes(),requse.getBytes().length,
InetAddress.getByName(IP) ,port);
//发送数据报
datagramSocket.send(requsePacket);
//创建对象用于接收服务发回的数据报
DatagramPacket repousePacket = new DatagramPacket(new byte[1024],1024);
//将获取到的数据报填充到数据报中
datagramSocket.receive(repousePacket);
//打印
String repouse = new String(repousePacket.getData(),0,repousePacket.getLength());
System.out.printf("req : %s : reps : %s\n",requse,repouse);
}
}
public static void main (String[] args) throws IOException {
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1",8848);
udpEchoClient.start();
}
}
好了 代码大功告成 我们来尝试着运行一下这个代码
我们也可以为这个回显服务来升级一下 我们来做一个简单的英文翻译词典
实现思路很简单 我们之前已经完成了回显服务器 我们只需要对返回方法进行处理即可 看以下代码
public class UDPEchoDicServer extends UDPEchoServer{
//为了方便查询 也为了可以保证我们的查找速度 我们选择哈希表来做一个数据存储
private Map<String,String> map = new HashMap<>();
public UDPEchoDicServer (int port) throws SocketException {
super(port);
//我们只做一个简单的词库
map.put("dog","小狗");
map.put("cat","小猫");
map.put("synchronized","同步");
}
@Override
public String process (String requse) {
//因为我们继承了处理后返回方法 所以直接继承之后重写即可
return map.getOrDefault(requse,"暂时未学习该单词");
}
public static void main (String[] args) throws IOException {
UDPEchoDicServer udpEchoDicServer = new UDPEchoDicServer(8848);
udpEchoDicServer.start();
}
}
接下来完成我们的客户端代码
public class UDPEchoClient {
private DatagramSocket datagramSocket = null;
private String IP;//目的IP
private int port;//目的端口
public UDPEchoClient(String IP,int port) throws SocketException {
//此处我们不分配端口号 为了我们的客户端程序可以主动分配一个可用的端口号
datagramSocket = new DatagramSocket();
this.IP = IP;
this.port = port;
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
while(true){
//提醒客户端输入
System.out.printf("-->");
//获取客户端的输入
String requse = scanner.nextLine();
//打包为UDP数据报 其中要放入要传入的目的端口 目的IP
DatagramPacket requsePacket = new DatagramPacket(requse.getBytes(),requse.getBytes().length,
InetAddress.getByName(IP) ,port);
//发送数据报
datagramSocket.send(requsePacket);
//创建对象用于接收服务发回的数据报
DatagramPacket repousePacket = new DatagramPacket(new byte[1024],1024);
//将获取到的数据报填充到数据报中
datagramSocket.receive(repousePacket);
//打印
String repouse = new String(repousePacket.getData(),0,repousePacket.getLength());
System.out.printf("req : %s : reps : %s\n",requse,repouse);
}
}
public static void main (String[] args) throws IOException {
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1",8848);
udpEchoClient.start();
}
}
有细心的小伙伴已经发现了 其实我们的客户端并没有做任何修改 我们直接接受返回的结果即可 至于为啥 嘿嘿 这就好比我们使用一款软件 这个软件其实更新的频率并不低 但是我们并不明确他更新的代码是什么 功能有时候也并不明确 但是客户端只需要使用就可以了呀 完全不影响我们的使用
我们来看一下效果吧
TCP
ServerSocket
提供给服务器使用的类(专门给服务器用的)
Socket
主要通过这样的类描述一个socket文件即可 而不需要专门的类表示"传输的包" 因为是面向字节流 以字节为单位传输的(既需要给服务器用 又需要给客户端用)
我们来尝试写一下TCP版本的代码
我们还是做一个TCP版本的回显服务器 之后再升级为词典服务器
public class TCPEchoServer {
private ServerSocket socket;
public TCPEchoServer(int port) throws IOException {\
//创建唯一的端口号
socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//获取客户端发送来的连接
Socket clientSocket = socket.accept();
//处理连接后的工作
SocketConnection(clientSocket);
}
}
private void SocketConnection (Socket clientSocket) throws IOException {
System.out.printf("[%s : %d]客户端上线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//采用此方法能大大减少代码量
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//我们约定当输入回车的时候结束输入
//当客户端断开连接后我们服务器也要跟客户端断开连接
if(!scanner.hasNext()){
System.out.printf("[%s : %d]客户端下线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String requse = scanner.next();
String repouse = process(requse);
printWriter.write(repouse);
printWriter.flush();//这个刷新一定要 否则客户端无法知道数据已经传输完毕
System.out.printf("[%s : %d] req : %s : reps %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),requse,repouse);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//与客户端断开连接 也就是释放这个连接
clientSocket.close();
}
}
public String process (String requse) {
return requse;
}
public static void main (String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(8878);
tcpEchoServer.start();
}
}
ok 我们当前已经完成了服务器的代码 为了验证服务器 我们来做一个客户端端口
public class TCPEchoClient {
private Socket socket;
public TCPEchoClient(String ip,int port) throws IOException {
socket = new Socket(ip,port);
}
public void start(){
System.out.println("客户端已经连接成功");
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner SocketScanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
System.out.printf("-->");
String requse = scanner.next();
printWriter.println(requse);
printWriter.flush();
String repouse = SocketScanner.next();
System.out.printf("req : %s : reps : %s\n",requse,repouse);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main (String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1",8878);
tcpEchoClient.start();
}
}
大功告成 我们来看一下代码效果
当我们客户端连接的时候 我们的服务器这边会显示到有客户端连接到 那么我们思考一个问题 我们对于一个服务器 是应该一对一服务一个客户端呢 还是一个服务器服务多个客户端 yes 就是要服务多个客户端 而我们当前的代码还有这个致命的缺陷 当前的服务器只能进行一对一的服务 无法做到一对多的服务
而我们也是知道的 当对于一个代码编译第二次的时候会提醒你重新编译运行吗 我们只需要这样操作即可打开能打开多个窗口的功能 ~
做完以上操作之后 我们来尝试的运行多个客户端吧
我们会发现当第二个客户端打开的时候 我们是无法进行连接处理的 这是因为我们在编写服务器代码的时候 采用了一个where循环当有一个客户端连接进来之后 其他服务只能一直等着这个客户端断开连接
所以 为了解决这个问题 我们决定引入多线程 每当一个客户端发起连接的时候 我们创建一个线程去处理这个 并不会影响我们继续接收其他客户端发来的连接请求 我们将这一段代码进行简单的修改
好 我们再来看一下加入多线程之后的效果
我们创建了五个客户端 并且对服务器发送请求都被一一处理掉了
问题又来喽
UDP的服务器会不会出现这样的问题呢?
答案是不会的 这又要说起我们的UDP和TCP 他们之间的不同 UDP是无连接的 他只会处理接收来的请求 但是TCP是有连接的 要先进行连接才可以进行处理
我们再来实现一个TCP的词典服务器吧~
public class TCPEchoDicServer extends TCPEchoServer{
private Map<String,String> map = new HashMap<>();
public TCPEchoDicServer (int port) throws IOException {
super(port);
map.put("dog","小狗");
map.put("cat","小猫");
map.put("synchronized","同步");
}
@Override
public String process (String requse) {
return map.getOrDefault(requse,"暂未学习该单词");
}
public static void main (String[] args) throws IOException {
TCPEchoDicServer tcpEchoDicServer = new TCPEchoDicServer(8888);
tcpEchoDicServer.start();
}
}
还是刚刚的思路 客户端的代码我们还是不变 我们直接上效果图
总结一下 这篇博客主要总结的是TCP和UDP的区别 以及举例了socket代码的实现来凸显他们之间的区别