目录
1.网络编程基础
1.1为什么需要网络编程?
1.2什么是网络编程
1.3网络编程中的基本概念
2.Socket套接字
2.1 分类
3.UDP数据报套接字编程
3.1 DatagramSocket API
3.2DatagramPacket API
3.3基于 UDP socket 写一个简单的回显客户端服务器程序(一发一收)
3.3.1UDP 服务端
3.3.2UDP客户端
3.3.3 服务器和客户端它们的交互过程
3.3.4整体效果演示
3.3.5一个服务器是可以同时给多个客户端提供服务的
3.4 写一个简单的单词翻译服务器(请求是一个英文单词,响应是这个单词的中文翻译)
1.网络编程基础
1.1为什么需要网络编程?
用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。
与本地打开视频文件类似,只是视频文件这个资源的来源是网络。相比本地资源来说,网络提供了更为丰富的网络资源:所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
1.2什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。
1.3网络编程中的基本概念
1️⃣发送端和接收端
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
2️⃣请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送 第二次:响应数据的发送
3️⃣客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
2.Socket套接字
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
2.1 分类
1️⃣流套接字:使用传输层TCP协议
有连接:使用 TCP 通信的双方,则需要刻意保存对方的相关信息可靠传输面向字节流:以字节为传输的基本单位,读写方式非常灵活全双工:一条路径,双向通信
2️⃣数据报套接字:使用传输层UDP协议
全双工:双向通信,一个管道,能 A->B,B->A 同时进行半双工:单向通信,一个管道,同一时刻,要么 A->B,要么 B->A ,不能同时进行无连接:使用 UDP 通信的双方,不需要可以保存对端的相关信息
不可靠传输
面向数据报:以一个 UDP 数据报为基本单位
全双工:一条路径,双向通信
3.UDP数据报套接字编程
3.1 DatagramSocket API
Datagram——数据报
Socket——说明这个对象是一个 socket 对象(相当于对应到系统中一个特殊的文件(socket文件),socket 文件并非对应硬盘上的某个数据存储区域)
DatagramSocket 是 UDP Socket,用于发送和接收UDP数据报
3.2DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报。
🌈构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
3.3基于 UDP socket 写一个简单的回显客户端服务器程序(一发一收)
回显服务器(echo server) 客户端发了一个请求,服务端返回一个一模一样的响应;一个服务器主要做三个核心工作:
1️⃣读取请求并解析
2️⃣根据请求计算响应(省略了)
3️⃣把响应返回到客户端
3.3.1UDP 服务端
socket = new DatagramSocket(port);
1️⃣绑定一个端口 => 把这个进程和一个端口号关联起来
一个操作系统上面,有很多端口号,0 - 65535 。
程序如果需要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符。(操作系统收到网卡数据,就可以根据网络数据报中的端口号,来确定要把这个数据交给哪个进程)
2️⃣分配端口号的过程:
手动指定:
new DatagramSocket(port);
系统自动分配:
socket = new DatagramSocket();
一个端口,在通常情况下,是不能被同一个主机上的多个进程同时绑定的;一个进程是可以绑定多个端口的。
如果端口已经被别人占用,再尝试绑定,就会抛出异常throws SocketException
读取客户端发来的请求,尝试读取,不是说调用了就一定能读到
//1.读取客户端发来的请求socket.receive();
对请求进行解析,把 DatagramPacket 转成一个 String如果客户端没有发来请求,receive 就会阻塞等待,直到真的有客户端的请求过来了,receive 才会返回。
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
String response = process(request);
通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理
public String process(String req){return req;}
把响应构造成 DatagramPacket 对象(构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
这个也是构造 DatagramPacket 的一种方式,先是拿字符串里面的字节数组,来构造 Packet 的内容,还要把请求中的客户端地址拿过来,也填到包裹里去。
response.getBytes().length 可以写作 response.length 吗?
不行 response.getBytes().length 表示的是字节数,response.length 表示的是字符数
requestPacket.getSocketAddress() -> (地址)IP + 端口
把这个 DatagramPacket 对象返回给客户端
socket.send(responsePacket);System.out.printf("[%s:%d] req = %s; resp = %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
UDP 服务端总代码:
package Network;import java.io.IOException;import .DatagramPacket;import .DatagramSocket;import .SocketException;import java.nio.charset.StandardCharsets;/*** Created with IntelliJ IDEA.* Description:* User: Lenovo* Date: -04-02* Time: 16:05*///UDP 服务端public class UdpEchoServe {//需要先定义一个 socket 对象:通过网络通信,必须要使用 socket 对象private DatagramSocket socket = null;//绑定一个端口,不一定能成功//如果某个端口已经被别的进程占用了,此时这里的绑定操作就会出错//同一个主机上,一个端口同一时刻,只能被一个进程绑定public UdpEchoServe(int port) throws SocketException {//构造 socket 的同时,指定要关联/绑定的端口socket = new DatagramSocket(port);}//启动服务器的主逻辑public void start() throws IOException {System.out.println("服务器启动!");//一旦服务器已启动,调用 start 方法,就会立即执行到 receive;//此时,还没有客户端达赖数据,此时 receive 就会阻塞等待,直到传输过来数据while (true) {//每次循环,要做三件事://1.读取请求并解析(构造空饭盒)DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//空的对象,空的对象持有了一个空的字节数据//在Java中,[] 和 . 是解引用;直接 = 不是,内部要想影响到外部,就需要通过解引用进行的//receive 参数类型是 DatagramPacket,是引用参数,即receive 方法内部,针对参数进行修改,外部也生效socket.receive(requestPacket);//为了方便处理这个请求,把数据报转换成 String//这个操作不是必须的,只是此处为了后续代码简单,就简单构造一个String,拿着requestPacket 中持有的字节数组进行构造String request = new String(requestPacket.getData(), 0, requestPacket.getLength());//2.根据请求计算响应(此处省略这个步骤)String response = process(request);//3.把响应结果写回到客户端:根据 response 字符串,构造一个 DatagramPacket//和请求 packet 不同,此处构造相应的时候,需要指定这个报要发给谁DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),requestPacket.getPort(), request, response);}}//这个方法希望是根据请求计算响应//由于写的是一个 回显 程序,请求是啥,响应是啥//如果后续写个别的服务器,不再回显了,而是有具体的业务了,就可以修改 process 方法,根据需要来重新构造响应//之所以单独列成一个方法,这是一个服务器中的关键环节private String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServe udpEchoServe = new UdpEchoServe(9090);udpEchoServe.start();}}
3.3.2UDP客户端
服务器,端口一般是手动指定的,如果自动分配,客户端就不知道服务器的端口是啥了,因此服务器有固定端口客户端才好访问。
客户端,端口一般是自动分配的,客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,说不好这个端口就和其他程序冲突了,导致咱们的代码无法运行。
public UDPEchoClient() throws SocketException {//客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好socket = new DatagramSocket();}
让客户端从控制台获取一个请求数据
System.out.println("> ");String request = scanner.next();
把这个字符串请求发送给服务器,构造 DatagramSocket,构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里(另外一种 DatagramPacket 的构造方法)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);
InetAddress.getByName(“127.0.0.1”) : 通过这个字符串来构造的 InetAddress,此处的 127.0.0.1 回环 IP 就表示当前主机。
8000 : 服务器端口号
这个包裹,就是要从客户端发送给服务器,就需要知道,发送的内容,以及发送的目的地是哪里(收件人地址 + 端口)
把数据报发送给服务器
socket.send(requestPacket);
从服务期读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);socket.receive(responsePacket);
把响应数据获取出来,转成字符串
String response = new String(responsePacket.getData(),0,responsePacket.getLength());System.out.printf("req: %s;resp: %s\n",request,response);
UDP 客户端总代码:
import java.io.IOException;import .*;import java.util.Scanner;public class UdpEchoClient {private DatagramSocket socket = null;private String serverIP;private int serverPort;// 客户端启动, 需要知道服务器在哪里!!public UdpEchoClient(String serverIP, int serverPort) throws SocketException {// 对于客户端来说, 不需要显示关联端口.// 不代表没有端口, 而是系统自动分配了个空闲的端口.socket = new DatagramSocket();this.serverIP = serverIP;this.serverPort = serverPort;}public void start() throws IOException {// 通过这个客户端可以多次和服务器进行交互.Scanner scanner = new Scanner(System.in);while (true) {// 1. 先从控制台, 读取一个字符串过来// 先打印一个提示符, 提示用户要输入内容System.out.print("-> ");String request = scanner.next();// 2. 把字符串构造成 UDP packet, 并进行发送.DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,InetAddress.getByName(serverIP), serverPort);socket.send(requestPacket);// 3. 客户端尝试读取服务器返回的响应DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);// 4. 把响应数据转换成 String 显示出来.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 udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);UdpEchoClient udpEchoClient = new UdpEchoClient("42.192.83.143", 9090);udpEchoClient.start();}}
3.3.3 服务器和客户端它们的交互过程
1️⃣服务器先启动,执行到 receive 进行阻塞
2️⃣由于阻塞,服务器接下来的代码就不执行了,此时执行客户端:客户端通过 Scanner 读取请求执行send 操作
3️⃣此时,客户端和服务器都会往下执行代码:客户端执行receive 读取响应,会阻塞等待;服务器从 receive 返回,读到请求数据(客户端发来的),执行到 process 生成响应,执行到 send 发送请求,并且打印日志
4️⃣服务器进入下一轮循环,再次阻塞在 receive,等待客户端下一次请求;客户端真正收到服务器 send 回来的数据后就会解除阻塞,执行下边的打印日志
4️⃣客户端继续进行下一轮循环,阻塞在 Scanner. next 这里等待用户输入新的数据
3.3.4整体效果演示
先启动服务器,再启动客户端客户端中输入一个hello:
在服务器中:
继续在客户端中输入一个你好:
服务器中显示:
3.3.5一个服务器是可以同时给多个客户端提供服务的
在 idea 中启动多个客户端,需要配置,默认一个程序只能启动一个
如在 IDEA 中,你想打开多个客户端,你发现你再运行一次客户端,就会把之前的客户端给关闭了,此时我们需要设置一下,就可以启动多个客户端。
此时我们就可以打开多个客户端了
3.4 写一个简单的单词翻译服务器(请求是一个英文单词,响应是这个单词的中文翻译)
import java.io.IOException;import .SocketException;import java.util.HashMap;import java.util.Map;//字典服务器 / 翻译服务器//希望实现一个英译汉的效果//请求的是一个英文单词,响应是对应的中文翻译public class UDPDicServer extends UDPEchoServer{private Map<String, String> dic = new HashMap<>();public UDPDicServer(int port) throws SocketException {super(port);//这里的数据可以无限的构造下去//即使是有道词典这种,也是类似的方法实现(打表)dic.put("cat","小猫");dic.put("dog","小狗");dic.put("fuck","卧槽");}//和 UDPEchoServer 相比,只是 process 不同,就重写这个方法即可public String process(String req){return dic.getOrDefault(req,"这个词俺也不会!");}public static void main(String[] args) throws IOException {UDPDicServer server = new UDPDicServer(8000);server.start();}}