协议是非常重要的芝士
协议分层,->解耦
TCP/IP五层协议
- 物理层 基础设施
- 数据链路层 两个相邻节点之间的数据传输
- 网络层 两个节点之间的路径规划
- 传输层 通信中的起点和重点
- 应用层 传过去的数据咋用
下层协议给上层提供服务,上层协议调用下层协议
封装和分用->发送和接受数据
网络编程(突破一台主机的限制)
一些网络编程中的基础概念
- 网络编程:两个/多个 进程,通过网络,来进行相互通信(写代码来实现)
进程具有隔离性(每个进程有自己独立的虚拟地址空间)
进程间通信->借助一个每个进程都能够访问到的公共区域,完成数据交换
网络编程也就是一种进程间通信的方式.
借助的公共区域就是网卡.是当下最主流的方式👇
既能够让同一个主机的多个进程间通信
也可以让不同主机的多个进程间通信
高并发,分布式,大数据
- 客户端(client)/服务器(server)
客户端:主动发送网络数据的一方
服务器:被动接受网络数据的一方
因为服务器无法知道客户端啥时候发来数据
因此就只能长时间运行,甚至7*24小时运行!
- 请求(request)/响应(response)
请求:客户端给服务器发送的数据
响应:服务器给客户端返回的数据.
- 客户端和服务器之间的交互方式
1)一问一答(最常见的方式)
客户端给服务器发送个请求
服务器给客户端返回个请求
2)多问一答(更少见一些,比如上传文件)
客户端发送多个请求
服务器返回一个响应
3)一问多答(出现的还行,比如播放视频,你点击一下,会返回弹幕,视频内容)
客户端发送一个请求
服务器返回多个相应
4)多问多答(远程控制,游戏串流)
客户端发送多个请求
服务器返回多个响应
进行网络编程,需要使用操作系统提供的网络编程API
传输层提供了两个非常重要的协议,截然不同
TCP
UDP
这两个协议对应的socket API也是截然不同的
如果谈到操作系统,最爱靠的就是多线程
那么谈到网络,最常考的就是TCP
简单概括
TCP:有连接,可靠传输,面向字节流,全双工
UDP:无连接,不可靠传输,面向数据包,全双工
有连接:打电话.先建立连接,然后再通信
无连接:发微信.不必建立连接,直接通信即可
(网络通信是无法保证百分之百到达的)
可靠传输:数据对方收没收到,发送方能够有感知
不可靠传输:数据对方收没收到也不管, 不知道~
比如打电话,就是可靠传输
比如发微信,也就是不可靠传输
面向字节流:这里的字节流和文件那里的字节流是一样的(不光概念上是一样的,连代码编写都是一样的)
面向数据报:以数据报为传输的基本单位
全双工:双向通信,一个管道,能A->B,B->A 同时进行
半双工:单向通信,一个管道,同一时刻,要么A->B,要么B->A 不能同时进行
为啥一个管道能够双向通信,你想想公路就知道了.
网线,标准的以太网线,里面其实是八根铜线
有没有做过水晶头
打开之后就可以传输数据了
send():发送数据
receive():接收数据
close():关闭文件
DatagramPacket :
表示一个UDP数据报
UDP是面向数据报的协议
传输数据,就是以DatagramPacket为基本单位
InetSocketAddress IP地址+端口号
写一个UDP版本的回显服务器-客户端(echo server)
客户端发啥,服务器就返回啥~~
绑定一个端口,把这个进程和一个端口号关联起来
一个操作系统上面,有很多端口号.0-65535
程序如果需要进行网络通信,就需要获取到一个端口号
端口号相当于用来再网络上区分进程的身份标识符
(操作系统收到网卡的数据,就可以根据网络数据报中的端口号,来确定要把这个数据交给哪个进程)
分配端口号的过程
- 程序员手动指定
- 系统自动分配 new DatagramSocket();()系统会自动分配一个空闲的端口
一个端口,在通常情况下,是不能被同一个主机上的多个进程同时绑定的
一个进程是可以绑定多个端口的.
如果端口已经被占用的话
读取客户端发来的请求,尝试读取,不是说调用了一定能读到
如果客户端没有发来请求,receive就会阻塞等待
知道真的有客户端请求过来了,receive才会返回
这个方法是通过参数来放置读取到的数据的,而不是通过返回值.
通过后面的new,就构造了一个空的DatagramPacket
输入输出的时候很容易发生异常.
一个服务器最容易坏的就是硬盘,特别是机械硬盘
👆这是最新的
String的构造方法,String(byte[], int)这个版本的构造方法,是被舍弃的
Java版本一直在更新,出新的东西的同时,也在舍弃旧的东西
旧的东西不能立即就删,得给程序员留下缓冲的时间.
先标记为被舍弃,也就是不建议程序员继续使用,可能会在未来的版本中删掉)👇
这个注解就是被舍弃的意思
这个也是一种构造DatagramPacket的方式
先是拿字符串俩面的字节数组中,来构造Packet的内容
还要把请求中的客户端的地址拿过来,也填写到包中.
把数据发出去
必须使用下面的哪个length,下面的表示字节数,而上面的表示字符数
//如果是服务器的端口号一般是指定的,客户端的端口号一般是自动分配的
//如果服务器自动分配,客户端就不知道服务器的端口是啥了.
//因此,服务器有固定的端口,客户端才方便访问
//客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序是不可控的
//如果要是手动指定端口,说不好这个端口就和其他程序的端口冲突了
//就导致我们的代码无法运行了.
这是另外一种DatagramPacket的构造方式
getByName此处的127.0.0.1是环回IP.就表示当前主机.
端口号就填写服务器的端口号
因为这个包裹是要从客户端发送给服务器的
所以就要知道发送的内容以及发送给的目的地是在哪里(收件人地址+端口)
目前我们已经见到了三个版本的DatagramPacket的构造
- 只太填写缓冲区,用来接收数据的,是一个空的PACKET
- 填写缓冲区,并且填写把包发给谁,InetAddress对象来表示的
- 填写缓冲区,并且填写把包发给谁,InetAddress+port
先理清楚,客户端和服务器的工作流程
服务器:
- 读取请求并解析
- 根据请求计算响应
- 构造响应数据,并返回给客户端.
客户端的核心流程
- 根据用户输入构造请求
- 把请求发送给服务器
- 读取服务器返回的响应
- 解析响应,并显示给用户
客户端和服务器的核心流程是紧密相连的.
上述流程,不仅仅是回显服务器客户端如此,大部分的客户端服务器都是如此
这都是一台基本套路
我们学习网络编程
- 学习网络编程的基本套路(核心流程)
- 学习socket api的使用
一个服务器是可以同时给多个客户端提供服务的
如果我现在不是想写一个回显服务器了,而是一个带有业务逻辑的服务器,怎么实现呢?
什么叫业务逻辑呢?
我们当前的回显服务器,是没有业务逻辑的
请求和回想都一样
正经的服务器,应该是请求和响应都不一样的,这样才有意义
主要就是在这个方法里面进行修改
我们新写一个类,继承这个服务器
重写一下process
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","小狗"); } @Override public String process(String req) { return dict.getOrDefault(req,"这个词俺也不会"); } public static void main(String[] args) throws IOException { UDPDictServer server = new UDPDictServer(8000);//8000这个端口相当于被我们占用了 server.start();//启动服务器 } }
一个服务器地灵魂所在
一个服务器要完成地工作,都是通过"根据请求计算响应"来体现地
不管是啥样地服务器,读取请求并解析,构造响应并返回,这两个步骤,大同小异
以下是本篇博客涉及到的两个服务器和一个客户端代码
package network; import java.io.IOException; import java.net.*; import java.util.Scanner; public class UDPEchoClient { private DatagramSocket socket = null; public UDPEchoClient()throws SocketException { //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,但还是自动分配比较好 //如果是服务器的端口号一般是指定的,客户端的端口号一般是自动分配的 //如果服务器自动分配,客户端就不知道服务器的端口是啥了. //因此,服务器有固定的端口,客户端才方便访问 //客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序是不可控的 //如果要是手动指定端口,说不好这个端口就和其他程序的端口冲突了 //就导致我们的代码无法运行了. socket = new DatagramSocket(); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); while (true){ //1.让客户端从控制台读取一个请求数据 System.out.print("> "); String request = scanner.next(); //2.把这个字符串请求发送给服务器,构造DatagramPacket // 我们构造的Packet既要包含要传输的数据,又要包含把数据发到哪里 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000); //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.printf("req: %s;resp: %s\n",request,response); } } public static void main(String[] args) throws IOException { UDPEchoClient client = new UDPEchoClient(); client.start(); } }
package network; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; public class UDPEchoServer { //要想创建UDP服务器,首先要先打开一个socket文件 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.对请求进行解析,把DatagramPacket转成一个String String request = new String(requestPacket.getData(),0,requestPacket.getLength()); //3.根据请求,处理响应.虽然咱们这个是个回显服务器,但是还是可以单独搞个方法来做这个事情 String response = process(request); //4.把响应构造成DatagramPacket对象 // 构造响应对象,要搞清楚,对象要发给谁,谁给咱们发的请求,我们就把响应发给谁. DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress()); //5.把这个DatagramPacket对象返回给客户端 socket.send(responsePacket); System.out.printf("[%s:%d] req=%s;resp=%s\n",responsePacket.getAddress().toString(),responsePacket.getPort(), request,response);//IP和端口,请求和响应 } } //根据这个方法,实现根据请求计算响应 这个过程 //由于是回显服务器,所以不涉及到其他裸机 //但是如果是其他服务器,就可以在process里面,来加上一些其他逻辑的处理 public String process(String req){ return req; } public static void main(String[] args) throws IOException { //真正启动服务器,这个端口号说是随便写,但是也是有范围的.0-65535 //但是一般来说1024以下的端口,都是系统保留 //因此咱们自己写代码,端口尽量还是选择1024以上,65535以下的. UDPEchoServer server = new UDPEchoServer(8000);//8000这个端口相当于被我们占用了 server.start();//启动服务器 } }
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","小狗"); } @Override public String process(String req) { return dict.getOrDefault(req,"这个词俺也不会"); } public static void main(String[] args) throws IOException { UDPDictServer server = new UDPDictServer(8000);//8000这个端口相当于被我们占用了 server.start();//启动服务器 } }