前言
前面我们学习了网络原理的初识,数据在网络中是如何传输以及接收的,那么知道网络原理了之后,就可以根据这些知识来实现网络编程了,现在很多应用都离不开网络,而作为程序员学习如何网络编程是很重要的,今天我将为大家分享关于网络编程套接字的知识。
为什么需要网络编程
网络编程是软件开发的一个重要领域,它允许不同的计算机之间进行数据交换和共享资源。通过网络编程可以做以下事情。
数据共享:网络编程使不同的计算机可以共享文件、数据和资源。这使得分布式数据管理、远程文件访问和数据同步变得更加容易。
分布式系统:网络编程有助于构建分布式系统,其中不同的计算机协同工作作为一个整体来执行任务。这可以提高系统的可靠性和可扩展性,并允许在多台计算机上分配计算和存储资源。
远程过程调用:网络编程允许一台计算机上的程序调用另一台计算机上的函数或方法。这使得开发人员可以编写更加模块化和可重用的代码,并实现更高效的计算和数据处理。
客户端-服务器架构:网络编程实现了客户端和服务器之间的通信。客户端发出请求,服务器响应请求并提供数据或服务。这种架构允许将应用程序或服务部署在多台计算机上,以提高性能、可扩展性和可靠性。
互联网应用开发:网络编程是开发互联网应用的关键。Web服务器、HTTP协议、网页脚本语言(如HTML、JavaScript)和服务器端脚本语言(如PHP、Python)等都使用网络编程概念。
通信和消息传递:网络编程允许不同的计算机之间建立可靠的通信通道,并通过消息传递机制进行数据交
换。这对于实现实时系统、远程监控、控制和报警系统等非常有用。
异步和并发:网络编程支持异步和并发操作,使得多台计算机可以同时处理多个任务,提高系统的响应速度和整体性能。
什么是网络编程
网络编程就是使用IP地址,或域名,和端口连接到另一台计算机上对应的程序,按照规定的协议(数据格式)来交换数据。网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。
中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,当然最后再做一些处理。
当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
网络编程中的基本概念
发送端和接收端
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
请求和响应
请求(Request)是指客户端向服务器发送一个信息或指令,以获取或处理某些资源或数据。例如,在浏览器中输入一个网址并按下回车键,就是在向服务器发送一个HTTP请求,请求获取对应的网页内容。
响应(Response)是指服务器对收到的请求做出反应的过程。当服务器收到客户端发送的请求后,它会根据请求的内容进行处理,并返回相应的数据或结果给客户端。例如,当浏览器发送一个HTTP请求时,服务器会返回对应的网页HTML代码,这就是一个响应。
请求和响应是网络编程中的基本交互模式,它们通常遵循请求-响应循环,即客户端发送一个请求,服务器响应请求并返回结果,然后等待下一个请求的到来。在这个过程中,客户端和服务器之间的通信协议、数据格式和传输方式等都是需要考虑的重要因素。
客户端和服务端
客户端通常是指使用客户端软件或者浏览器等应用程序向服务器发起请求的计算机或设备。客户端向服务器发送请求,请求数据或者服务。例如,在浏览器中输入网址后,浏览器会向服务器发起请求,请求相应的网页内容。在游戏应用程序中,客户端会向游戏服务器发送请求,请求连接游戏服务并获取游戏数据。
服务端通常是指提供服务的计算机或设备。服务器接收客户端发送的请求,处理请求并返回相应的数据或服务。例如,网页服务器接收浏览器的请求,处理请求并返回相应的网页内容;游戏服务器接收游戏客户端的请求,处理请求并返回相应的游戏数据。
客户端和服务端的交互是计算机网络中应用层通信的基础。在C/S架构(即Client与Server,客户端与服务器端架构)中,客户端一般指应用程序,程序需要先安装后才能在用户的电脑上运行,对用户的电脑操作系统环境依赖较大。而在B/S架构(即Browser与Server,浏览器端与服务器端架构)中,只需在浏览器上通过HTTP去请求服务器端相关的资源(网页资源)。
常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
- 客户端先发送请求到服务端
- 服务端根据请求数据,执行相应的业务处理
- 服务端返回响应:发送业务处理结果
- 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)
Socket 套接字
什么是 Socket 套接字
Socket(套接字)是网络编程中的一个重要概念,它是应用程序进行网络通信的接口。套接字是应用进程之间进行双向通信的端点的抽象,它提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,也是应用程序与网络协议栈进行交互的接口。
在计算机网络中,套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。在C/S架构(即Client与Server,客户端与服务器端架构)中,每个套接字都与一个特定的IP地址和端口号关联。客户端应用程序使用套接字向服务器端发送请求,而服务器端则使用套接字监听来自客户端的请求,并对请求进行处理和响应。
套接字的分类
流套接字(SOCK_STREAM):流套接字用于提供面向连接、可靠的数据传输服务。它通过使用传输控制协议(TCP)来确保数据的无差错、无重复发送,并且数据能够按顺序接收。这种套接字类型适合传输大量的数据,但不支持广播和多播方式。
数据报套接字(SOCK_DGRAM):数据报套接字提供了一种无连接的服务,通信双方不需要建立任何显式连接,数据可以发送到指定的套接字,并且可以从指定的套接字接收数据。这种服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。这种套接字类型使用用户数据报协议(UDP)进行数据的传输。
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
传输层 TCP 协议和 UDP 协议的区别
1. TCP 是有连接的,UDP 是无连接的。
在计算机中,这种抽象的连接是很常见的,此处的连接本质上就是建立连接的双方各自保存对方的信息。两台计算机建立连接就是保存了对方的关键信息。
TCP 要想通信的话,就得双方建立连接,只有建立了连接之后才能继续后面的通信。
而 UDP 则不需要双方建立连接就可以直接进行后面的通信,他会直接发送数据,不会挣得对方的同意(因为这里 UDP 不会主动保存对方的信息,所以我们作为程序员,在写程序的时候要把对方的信息给传递过去)
2. TCP 是可靠传输,UDP 是不可靠传输
为什么会说会有可靠和不可靠传输呢?因为在网络上进行通信的时候,不可能做到数据的百分百传输,就像这一句话:再牛逼的技术,也抵不住挖掘机一铲子。
所以既然无法做到百分百传输,那么退而求其次:当数据没有传输成功的话我们需要知道数据没有传输成功,然后做出相应的措施,比如超时重传等。所以 TCP 就内置了可靠传输,而 UDP 则没有内置可靠传输。那么为什么 UDP 没有内置可靠传输呢?换一种说法就是:可靠传输有什么缺点吗?
可靠传输会付出什么代价呢?
机制更加复杂
传输效率会降低
3. TCP 是面向字节流的,而 UDP 则是面对数据报的
此处说的字节流和前面文件操作中的字节流是一个意思,TCP 是以字节为单位进行数据的传输,而 UDP 则是以数据报为单位进行数据的传输。
什么是数据报呢?数据报是网络通信数据的基本单位,并且数据的基本单位不止有数据报这个叫法,还有数据包、数据帧、数据段……这些都是可以表示网络通信中数据的基本单位,但是它们还是有些许区别的,我在这里就不为大家解释了。
4. TCP 和 UDP 都是全双工的
全双工是指一个信道可以双向通信,这就是全双工;而一个信道只可以单向通信则是属于单双工。
在Java中如何实现 UDP 数据报套接字编程
在 Java 中实现 USP 数据报套接字编程需要依赖 DatagramSocket API 。DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。socket其实也是属于操作系统中的一个概念,本质上是一个特殊的文件,Socket 就属于是把网卡这个设备抽象成一个文件了,往 socket 文件中写入数据,就相当于通过网卡发送数据;从 socket 文件中读取数据就相当于通过网卡接收数据。
这篇文章我将为大家分享一个 客户端-服务器 通信的程序,但是这个服务器没有什么业务逻辑,就是将客户端传来的数据再原封不动的返回。
服务端
1. 创建出 DatagramSocket 对象并指定端口号
要想创建出 DatagramSocket 对象,我们首先需要知道有什么构造方法。
方法签名 | 说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(intport) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
这是创建 DatagramSocket 对象时常见的两种构造方法,那么为什么会有指定端口号和不指定端口号的两种构造方法呢?因为这是 客户端-服务器 类型,作为程序员,我们肯定能知道我们的计算机中有哪些端口没有被占用,所以我们可以合理分配端口号;而作为用户,很多人根本不知道自己电脑上有哪些端口号可以用,如果用户传入的端口号被占用的话,那么这个 DatagramSocket 对象就会创建失败,那么此时用户并不知道该怎么做,他只会骂你这个程序员写出来的程序是垃圾。所以就有了这两种指定端口号和不指定端口号由计算机自己分配端口号的两种构造方法。
public class UdpEchoServer { //创建一个socket对象,后续网卡操作的基础 private DatagramSocket socket = null; public UdpEchoServer (int port) throws SocketException { //当创建DatagramSocket的时候,传入的port参数就表示手动指定端口号 this.socket = new DatagramSocket(port); } }
2. 实现服务端的 run 方法
run 方法是让服务端代码运行起来的接口。
public void run() { System.out.println("服务端启动"); while (true) { DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096); } }
这里为什么需要 while(true) 循环?因为服务器不知道客户端什么时候会发来请求,所以服务器就需要无时无刻不在处于工作状态。
因为 UDP 数据的传输和接收都是以数据报为单位来进行传输的,所以这里也就需要创建出数据报对象。
创建 DatagramPacket 对象的时候需要传入一个 byte 类型的数组以及这个数组的大小。
当创建好这个 DatagramPacket 之后,就可以实现接收客户端传来的数据了。
socket.receive(requestPacket);
这里可能有人会问了,服务器接收数据之后不是应该返回这个接收的数据吗?为什么这个 receive 方法的返回值类型是 void 类型呢?其实这里的 byte 数组参数是一个输出型参数,这个方法会将接收到的数据给传入到数组中。
//将接收到的二进制数据转换为字符串 String request = new String(requestSocket.getData(), 0 ,requestSocket.getLength());
当接收到数据之后,因为接收到的数据是二进制的形式,所以需要将二进制数据转换为字符串数据。
//服务端做出业务处理 String response = process(request);
服务端做出业务处理,这里只是简单的返回原字符串的业务。
private String process(String request) { return request; }
//将服务端做出的业务字符串打包成数据报,这里需要指定目的IP和目的端口号 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestSocket.getSocketAddress());
当服务器做出业务处理之后就需要将处理的数据再次打包成数据报返回,并且在这个数据包创建的过程中还需要指定目的IP和目的端口,这个目的IP和目的端口就可以用 getSocketAddress()
方法获取到。
//发送响应 socket.send(responsePacket);
将业务处理之后的数据使用 send 方法返回给客户端。
当服务器的业务逻辑完成之后,可以打印出日志。
System.out.printf("[%s:%d] req=%s res=%s",requestSocket.getSocketAddress(), requestSocket.getPort(), request,response);
3. 服务端全部代码
import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; public class UdpEchoServer { //创建一个socket对象,后面网卡操作的基础 DatagramSocket socket = null; public UdpEchoServer(int port) throws SocketException { //当服务器端创建socket的时候指定端口 this.socket = new DatagramSocket(port); } public void run() throws IOException { System.out.println("服务端启动"); //这里为什么要用while(true)循环,因为我们不知道什么时候客户端会发送来请求,所以服务端就需要无时无刻不在工作 while (true) { //创建一个数据报对象用来接收服务器端发送来的数据 DatagramPacket requestSocket = new DatagramPacket(new byte[4096], 4096); //接收数据,并且将接收的数据输出到requestSocket中 socket.receive(requestSocket); //将接收到的二进制数据转换为字符串 String request = new String(requestSocket.getData(), 0 ,requestSocket.getLength()); //服务端做出业务处理 String response = process(request); //将服务端做出的业务字符串打包成数据报,这里需要指定目的IP和目的端口号 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestSocket.getSocketAddress()); //发送响应 socket.send(responsePacket); System.out.printf("[%s:%d] req=%s res=%s",requestSocket.getSocketAddress(), requestSocket.getPort(), request,response); } } private String process(String request) { return request; }
客户端
1. 创建出 DatagramSocket 数据报对象
public class UdpEchoClient { //这里同样需要创建出一个DatagramSocket对象,后面网卡编程的基础 private DatagramSocket socket = null; //服务端IP private String serverIp = ""; //服务端端口 private int serverPort = 0; public UdpEchoClient(String serverIp, int serverPort) throws SocketException { //当创建DatagramSocket对象时,如果不传入参数的话,计算机就会自动分配未被占用的端口号 socket = new DatagramSocket(); //因为 UDP 在通信之前不需要建立连接,所以就需要我们主动添加目的IP和目的端口号 this.serverIp = serverIp; this.serverPort = serverPort; } }
因为客户端跟服务端的业务逻辑是有些许区别的,服务端是先接收数据,然后处理数据,最后才是发送数据;而客户端是先发送请求的一方,所以就需要先指定出目的端口和目的IP,这个是需要客户端手动指定的。
2. 实现包含客户端主要逻辑的 start 方法
public void start() throws IOException { System.out.println("客户端启动"); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("-->"); //输入请求 String request = scanner.next(); //根据输入的请求创建数据报 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp),serverPort); //发送请求 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.println(response); } }
3. 客户端整体代码
public class UdpEchoClient { //这里同样需要创建出一个DatagramSocket对象,后面网卡编程的基础 private DatagramSocket socket = null; //服务端IP private String serverIp = ""; //服务端端口 private int serverPort = 0; public UdpEchoClient(String serverIp, int serverPort) throws SocketException { //当创建DatagramSocket对象时,如果不传入参数的话,计算机就会自动分配未被占用的端口号 socket = new DatagramSocket(); //因为 UDP 在通信之前不需要建立连接,所以就需要我们主动添加目的IP和目的端口号 this.serverIp = serverIp; this.serverPort = serverPort; } public void start() throws IOException { System.out.println("客户端启动"); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("-->"); //输入请求 String request = scanner.next(); //根据输入的请求创建数据报 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIp),serverPort); //发送请求 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.println(response); } } public static void main(String[] args) { UdpEchoClient udpEchoClient = new UdpEchoClient() } }
理解这个套接字编程的整体逻辑
先启动服务端,当服务端启动的时候,会执行到 receive 方法,但是由于客户端并没有发来请求,所以客户端的代码就会进入阻塞等待状态;然后启动客户端,提示用户输入请求数据,当用户输入数据之后,客户端会先将这些请求数据打包成数据报的形式,然后根据指定的目的IP和目的端口 send 给服务端,当服务端接收到客户端的请求数据报之后,会先对数据报进行解析,将数据报中的二进制数据解析为字符串类型,然后执行 process 业务逻辑,并且返回响应数据,服务端又会将返回的响应数据打包成数据报的形式根据源IP和源端口将数据报返回给客户端;当客户端接收到数据报形式的数据,也是先将二进制的数据解析为字符串,然后打印出这个数据。
为什么这里没有用到 close 方法关闭 socket
这里的 socket 既然是一个文件,如果频繁的创建而不销毁的话,就会占据大量的文件描述表的内容,可能会造成文件资源的泄露,但是这里为什么不需要 close 呢?因为这里 socket 文件在整个程序运行的过程中都会被使用到的,如果 socket 文件被关闭就意味着程序已经运行结束了,所以根本谈不上内存泄漏。
解决因字符编码不同的问题出现乱码的问题
因为是网络编程,所以就需要指定数据的编码格式,否则就会出现乱码的问题,所以我们就在所有与字符编码相关的地方指定相同的编码 UTF-8。
String request = new String(requestSocket.getData(), 0 ,requestSocket.getLength(),"UTF-8");
DatagramPacket responsePacket = new DatagramPacket(response.getBytes("UTF-8"), response.getBytes().length, requestSocket.getSocketAddress());
代码运行结果