目录
在上一节中小编主要是与大家分享了一些有关于网络的基础知识,但是里面的细节和基础的编程还没有给大家来交代,这节中小编就给大家俩交代一下有关于网络基础编程方面的一些基础的编程吧,大家赶快跟上小编的步伐一起来往下看吧。如果还没有看小编网络基础知识的部分的同学建议先去看看这篇博文吧:☞http://t.csdn.cn/aj9ov
1.1为什么需要网络编程
用户在浏览器中,打开在线视频网站,比如抖音短视频其实质是通过网络,获取到网络上的一个视频资源,与本地打开视频文件类似,只是视频文件这个资源的来源是网络,相比本地资源来说,网络提供了更为丰富的网络资源,所谓的网络资源,其实就是在网络中可以获取的各种数据资源,而所有的网络资源,都是通过网络编程来进行数据传输的。
1.2什么是网络编程
网络编程:指网络上的主机,通过不同的进程,以编程的方式实现网络通信(网络数据传输)。
当然,我们只要满足进程不同就行,所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
1.3网络编程中的基本概念
1.3.1发送端和接收端
在一次网络数据传输时:
- 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
- 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
- 收发端:发送端和接收端两端,也简称Wie收发端。
注意:发送端和接收端只是相对的概念,只是一次网络数据传输产生数据流向后的概念。
1.3.2请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输。
- 第一次:请求数据的发送。
- 第二次:响应数据的发送。
就像是在餐厅点饭一样,先发起请求:点一份蛋炒饭。餐厅在给一个响应:提供一份蛋炒饭。
1.3.3客户端和服务端
- 服务端:在常见的网络数据传输的场景下,把提供服务的这一方进程,称为服务端,可以提供对外服务。
- 客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
- 客户端获取服务资源。
- 客户端保存资源在服务端。
就像是我们在银行办事:
- 银行提供存款服务:用户(客户端)保存现金(资源)在银行(服务端)。
- 银行提供取款服务:用户(客户端)获取服务端资源(银行替用户保管现金)。
2.1概念
Socket套接字是操作系统提供给应用程序的一组用于网络编程的API。他是基于TCP/IP协议的通信的的基本操作单元。
注意:操作系统原生的Socket API是C语言的但是这里我们学习的是Java封装之后的版本。
2.2分类
Socket套接字主要针对传输层协议划分为如下三类:
- 数据报套接字:使用传输层UDP协议。
UDP,即User Datagram Protocol(用户数据报协议),传输层协议。它的特点是:无连接、不可靠传输、面向数据报、全双工。
- 流套接字:使用传输层TCP协议。
TCP,即Transmission Control Protocol(传输控制协议),传输层协议。它的特点是:有连接、可靠传输、面向字节流、全双工。
对于字节流来说,可以简单理解为传输的数据是基于IO流的,流式数据的特征就是在IO流没哟关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
- 原始套接字:
原始套接字用于自定义传出层协议,用于读写内核没有处理的IP协议数据,这里我们对此不做过多讨论,我们重点是理解和应用前两个。
TCP特点vsUDP特点:
UDP特点 | TCP特点 |
无连接:使用UDP通信双方不需要刻意保存对方的相关信息 | 有连接:使用TCP通信双方则需要刻意保存对方的相关信息 |
不可靠传输:消息发了就发了不关注结果 | 可靠传输:不是说发送之后对方就可以100%能够达到对方,这要求就太高了,只是说尽可能的传输过去。 |
面向数据报:以UDP数据报为基本单位。 | 面向字节流:以字节为传输的基本单位,读写方式非常灵活 |
全双工:一条路径,双向通信 | 全双工:一条路径,全向通信。 |
解释:全双工vs半双工。
- 全双工:是一条路径,全向通信,你可以理解为,一个双向通道的马路。
- 半双工:是一条路径,只能由一侧向另一侧通信,你可理解为单向通道的马路。
针对上述的TCP协议和UDP协议也给我们提供了两组不同的API。下面我们来一步一步的了解一下。
3.1DataGramSocket API
DataGramSocket 是UDP Socket,用于发送和接收UDP数据报,所谓Socket,是一个特殊的文件,是网卡这个硬件设备的抽象表示,你也可以理解为是一个遥控器,想要进行网络通信就需要有socket文件这样的对象,借助这个socket文件对象,才能够间接的操作网卡。
- 往这个socket对象里写数据,相当于通过网卡发送消息。
- 从这个socket对象中读数据,相当于通过网卡接收消息。
DatagramSocket的构造方法,可以绑定一个端口号(服务器),也可以不显示指定客户端。
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
- 服务器这边的socket往往要关联一个具体的端口号。
- 客户端这边则不需要手动指定,系统会自动分配一个闲置的端口号。
举个例子:
比如现在我开了一家餐厅,要发传单,那么在传单上面我这边可定是要标清楚我的餐厅的具体位置在哪,窗口号是多少,都得事先分配好,此时我开的这家餐馆就相当于是服务器,确定的地址和窗口号就是服务器事先分配好的端口号。那么如果此时客人看到我发的传单就来到我的餐馆吃饭了,那么它点完餐之后,就会随便找一个空着的位置坐下,等饭。此时客人就相当于是客户端,随便找的位置就是系统给随机分配的一个空闲的端口号。
DatagramSocket方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
void close() | 关闭数据报套接字(释放资源) |
注意:
- DatagramPacket表示一个UDP数据报。
- 在close的时候,到底啥时候调用close,一定是要socket/文件,确定一定以及肯定不再使用,此时才能调用close。
3.2DatagramPacket API
DatagramPacket是UDPSocket发送和接收的数据报。
DatagramPacket构造方法:
方法签名 | 方法说明 |
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int lenght, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length),address指定目的的主机的IP和端口号。 |
DatagramPacket方法:
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址,或从发送的数据报中,获取接收端主机IP地址。 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号,或从发送的数据报中,获取接收端主机端口号。 |
byte[] getData() | 获取数据报中的数据。 |
3.3基于UDP的回显服务器(echo server)
介绍了DatagramSocket 和 DatagramPacket API之后,我们基于UDP socket写一个简单的客户端服务器程序。 也就是让客户端发一个请求,在服务器上返回一个一模一样的响应。
首先来明确一点,一个服务器主要做的三个核心的工作:
- 读取请求并解析。
- 根据请求并计算响应。(代码中省略了)
- 把响应返回给客户端。
服务端代码:
package network;//服务端import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.SocketException;public class UdpEchoServer{ //需要先定义一个Socket对象 //通过网络通信,必须要使用socket对象 private DatagramSocket socket = null; //绑定一个端口,不一定能成功 //如果某个端口已经被别的进程占用了,此时这里的绑定操作就会出错。 //同一个主机上,一个端口,同一个时刻,只能被一个进程绑定 public UdpEchoServer(int port) throws SocketException { //构造socket的同时,指定要关联/绑定的端口。 socket = new DatagramSocket(port); } //启动服务器的主逻辑 public void start() throws IOException { System.out.println("服务器启动成功!"); while (true) { //每次循环,要做三件事 //1.读取请求并解析 // 构造空饭盒 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096); // 食堂大妈给饭盒里打菜(饭从网卡上来) //这里的receive会阻塞等待,等到客户端那边发送数据过来 socket.receive(requestPacket); //为了方便处理这个请求,需要把数据报转换成String String request = new String(requestPacket.getData(), 0, requestPacket.getLength()); //2.根据请求计算响应(此处省略这个步骤) String response = process(request); //3.把响应结果写回到客户端 // 根据response 字符串,构造一个DatagramPacket // 和请求packet 不同,此处构造响应的时候,需要指定这个包要发给谁,这里调用requestPacket.getSocketAddress()就可以得知了 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 { UdpEchoServer udpEchoServer = new UdpEchoServer(9090); udpEchoServer.start(); }}
客户端代码:
package network;//客户端import java.io.IOException;import java.net.*;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.println("->"); 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.start(); }}
启动服务器和客户端结果展示:
注意:这里一定是先启动服务器,再启动客户端!!!
执行流程如下所示:
注意:在上述过程中我们是客户端和服务器在同一个主机上,使用的是127这个IP,不同主机则就写实际的IP即可。
在上述通信过程中,站在客户端发送数据的角度:
- 源IP是:127.0.0.1
- 源端口是:64982,他是系统自动分配的空闲端口。
- 目的IP是:127.0.0.1
- 目的端口是:9090
在上述过程中就有同学好奇了不是说是要使用close来关闭资源的吗?为什么在代码中好像没有看到释放资源这一步,其实对于UdpEchoServer来说,这个socket对象是出了循环就不用了,但是循环结束,意味着start结束,意味着main方法结束,同时意味着进程结束,那么此时进程都结束了所以的资源也就自然释放了,所以就不必显示释放资源了。
3.4简单的翻译服务器
在上述中我们编写的是一个回显服务器,它是没有实际意义的。那么如何写一个提供实在价值的服务器呢?当响应和请求不一样了,响应是根据不同的请求计算得到的,这里就需要我们对上述过程没有写的process方法来进行编写,那么下来我们就具体来实现一下。我们就来编写一个简单的英文单词翻译服务器,请求是一个英文单词,响应是这个单词的中文翻译。
服务端代码展示:
package network;//词典查询服务端import java.io.IOException;import java.net.SocketException;import java.util.HashMap;import java.util.Map;//使用继承,是为了复用之前的代码public class UdpDicServer extends UdpEchoServer{ private Map dict = new HashMap<>(); public UdpDicServer(int port) throws SocketException { super(port); dict.put("dog", "小狗"); dict.put("cat", "小猫"); dict.put("tiger", "老虎"); //注意:这里可以无限添加很多个数据 } @Override public String process(String request) { return dict.getOrDefault(request,"该单词没有查到!"); } public static void main(String[] args) throws IOException { UdpDicServer udpDicServer = new UdpDicServer(9090); udpDicServer.start(); }}
客户端代码展示:
package network;//客户端import java.io.IOException;import java.net.*;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.println("->"); 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.start(); }}
运行结果展示:
注意:在上述编写服务端代码时我们是直接使用了继承,重写了父类的process方法。这样就减少了我们的工作。
在TCP中有两个核心的类:
- ServerSocket:是给服务器使用的。
- Socket:即会给客户端使用,又会给服务器端使用。
下面我们就来分别看看ServerSocket和Socket的具体使用方法。
4.1ServerSocket API
他是创建服务端使用的API。
SocketSocket构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务流套接字Socket,并绑定到指定端口 |
SocketSocket方法:
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待。 |
void close() | 关闭此套接字 |
这里的accept意思就是接收,在客户端主动向服务器发起连接请求,服务器就得同意一下,但是实际上的这个accept又和我们上述给大家解释的意思不太一样,这里的accept只是在应用层面的接收,实际的TCP连接的接受是在该内核里已经完成了。这个后面在将TCP的时候会给大家交代的。
4.2Socket API
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端的信息,及用来与对方收发数据的。
Socket的构造方法:
方法签名 | 方法说明 |
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。 |
这里的host和port指的是服务器的IP和端口,TCP是有连接的,在客户端new Socket对象的时候就会尝试和指定IP端口的目标建立连接了。
Socket的方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutPutStream getOutStream() | 返回此套接字的输出流 |
InputStream getInputStream()和OutPutStream getOutStream()是字节流,就可以通过上述字节流对象,进行数据传输了。
- 从 InputStream 这里读数据,就相当于是从网卡接收。
- 往 OutPutStream 这里写数据,就相当于从网卡发送。
注意:
这个Socket和DatagramSocket定位类似,都是构造的时候指定一个具体的端口,让服务器绑定该端口,但是ServerSocket一定要绑定具体的端口。
4.3基于TCP的回显程序
服务端代码展示 :
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 { //serverSocket只有一个,clientSocket会给每一个客户都分配一个 private ServerSocket severSocket = null; public TcpEchoServer(int port) throws IOException { severSocket = new ServerSocket(port); } public void start() throws IOException { ExecutorService executorService = Executors.newCachedThreadPool(); System.out.println("服务器启动成功!"); while (true) { Socket clientSocket = severSocket.accept(); //如果直接调用,该方法会影响这个循环的二次执行,导致accept不及时了。 //创建新的线程,用新线程来调用processConnection //每次来一个新的客户端都搞一个新的线程即可!// Thread t = new Thread(() -> {// try {// processConnection(clientSocket);// } catch (IOException e) {// e.printStackTrace();// }// });// t.start(); executorService.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }); } } //读取这个方法来处理一个连接 //读取请求 //根据请求计算响应 //把响应返回给客户端 private void processConnection(Socket clientSocket) throws IOException { System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); //使用try()这种写法,()中允许写多个流对象,使用;来分隔 try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { //没有这个scanner和printWriter,完全可以,但是代价就是得一个字节一个字节扣,找到哪个是请求结束的标记\n //不是不能做,而是代替代码比较麻烦 //为了简单,把字节流包装成了更方便的字符流 Scanner scanner = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream); while (true) { //1.读取请求 //采用hasNext判定接下来还有没有数据了,如果对端关闭了连接(客户端关闭连接),此时hasNext就会返回false,循环就结束 if (!scanner.hasNext()) { //读取的流到了结尾了(对端关闭了) System.out.printf("[%s:%d] 客户端下线了!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } //直接使用scanner读取一段字符串 //next会一直往后读,读到空白符结束(空格、换行、制表符、翻页符...都算空白符) //nextLine只是读到换行符结束,所以这里没有使用它 String request = scanner.next(); //2.根据请求计算响应 String response = process(request); //3.把响应写回给客户端,不要忘记了,响应也是要带上换行的 //返回响应的时候要把换行符加回来,方便客户端那边来区分从哪里到哪里是一个完整的响应。 printWriter.println(response); //flush当数据不够大的时候直接进行强制刷新,将缓冲区中的数据发给客户端 printWriter.flush(); System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); } }catch (IOException e) { e.printStackTrace(); }finally { //clientSocket只是一个连接提供服务的,这个还是要进行关闭的 clientSocket.close(); } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer = new TcpEchoServer(9090); tcpEchoServer.start(); }}
客户端代码展示:
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 port) throws IOException { //这个操作相当于让客户端和服务器建立TCP连接 //这里的连接连上了,服务器的accept就会返回 socket = new Socket(serverIP, port); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { PrintWriter printWriter = new PrintWriter(outputStream); Scanner scannerFromSocket = new Scanner(inputStream); while (true) { //1.从键盘上读取用户输入的内容 System.out.println("->"); String request = scanner.next(); //2.把读取到的内容构造成请求,发送给服务器 //注意,这里的发送,是带有换行的。 printWriter.println(request); printWriter.flush(); //3.从服务器读取响应的内容 String response = scannerFromSocket.next(); //4.把响应结果显示到控制台上 System.out.printf("req: %s; resp: %s\n", request, response); } } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090); client.start(); }}
结果展示:
执行流程如下所示:
那么这里我们只是启动了一个客户端,在实际中不可能是一个服务器只给一个客户端进行服务,那么如何启动多个客户端呢?这里在idea中是默认下只能启动一个的,那么这里我们需要打开idea配置一下。配置过程如下所示:
此时当我们再次点击上述的三角形就可以再次启动另一个客户端了。
回顾并理解我们为什需要协议
以上我们实现的UDP和TCP数据传输,除了UDP和TCP之外,程序还存在应用层定义协议,可以想想分别都是什么样的协议格式。
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
- 客户端发送请求和服务端解析请求和要使用相同的数据格式。
- 服务端返回响应和客户端解析响应也要使用相同的数据格式。
- 请求格式和响应格式可以相同,也可以不同。
- 约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。
- 可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。
这节中小编主要是和大家分享了网络编程中的两个重要的编程UDP和TCP,后期小编还会继续出有关于网络方面的知识的,希望这节对大家了解网络有一定帮助,想要学习的同学记得关注小编和小编一起学习吧!如果文章中有任何错误也欢迎各位大佬及时为小编指点迷津(在此小编先谢过各位大佬啦!)
来源地址:https://blog.csdn.net/weixin_61599986/article/details/132147846