那么在之前呢,小编也是介绍到了网络的一些相关知识,特别是TCP/IP模型,也是着重介绍了一番。
那么今天小编要分享的是基于TCP、UDP的网络编程。
那么对于网络编程,这个又是个什么东东呢?
网络编程:是指编写能够通过计算机网络进行通讯的程序。
那么接下来的网络编程讲解,将以服务器和客户端的模型来展开。
服务器:
是指在网络上提供资源和服务的计算机或程序。
可以是一台物理机器,也可以是在多台机器上运行的软件。
客户端:
是指那些用来访问服务器提供的资源和服务的应用程序或设备。
ok,首先以基于UDP的方式编写这个客户端和服务器
UDP
当然在此之前呢,那肯定先得了解下Java.net包提供对于UDP封装好的API吧
DatagramSocket
这个类是UDP用来进发送和接收数据报的。
构造方法:
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机一个指定端口 |
那么这里要对套接字解释下
套接字:
进程是计算机网络中用于实现不同主机之间进程通信的一种编程接口和技术。
方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p) | 用于接收UDP协议发送的数据报(没有收到,此时就会阻塞当前线程) |
void send(DatagramPacket p) | 基于UDP协议发送数据报(不阻塞当前线程,直接发送) |
void close() | 关闭数据报套接字 |
对于这个DatagramPacket,这个是一个UDP协议中,信息的载体
用来封装收到和发送的信息,这是因为,UDP是基于数据报的形式发送的。
DatagramPacket
构造方法:
方法签名 | |
DatagramPacket(byte[] buf, int length) | 构造⼀个DatagramPacket以⽤来接收数据报,接收的 数据保存在字节数组(第⼀个参数buf)中,接收指定 ⻓度(第⼆个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造⼀个DatagramPacket以⽤来发送数据报,发送的 数据为字节数组(第⼀个参数buf)中,从0到指定⻓ 度(第⼆个参数length)。address指定⽬的主机的IP 和端⼝号) |
方法:
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发 送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端⼝号;或从 发送的数据报中,获取接收端主机端⼝号 |
byte[] getData() | 获取数据报中的数据 |
这个DatagramSocket就像是一个遥控器
DatagramPacket就像是纸条传信息中的那张纸条。
那么这些个API介绍差不多后
现在可以来编写代码。
首先编写的是服务器代码。
这里编写的回显服务器和回显客户端
这两个东东,简单来说,客户端发送什么东西,服务器就返回什么东西,不作任何“加工”。
服务器
编写这个代码思路也是挺明了
就拿寄快递举个例子
我们平时去寄快递,那么一般也是在网上下单的,怎么下单呢?
那肯定要选中一个快递点先对不?
同样的,服务器中我们首先也要指定一个端口号,才能让数据往哪里发送
代码:
public class UdpServer {
DatagramSocket socket = null;
public UdpServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
}
接着下完单后,我就拿着东西过来寄了,我拿过来这个事,就像是客户端传来的数据。
既然数据来了,那么肯定要拿东西来接收它,这个承载的容器就是DatagramPacket
然后,东西接到手,那么就要对它处理下咯,比如消消毒,检查什么的。
处理完毕过后,此时呢,快递站点就要对它进行包装了,包装符合规范的物件进行发送
那么这里包装的时候,使用的还是DatagramPacket
包装完毕后,我们就发送数据出去。
发送完后,快递上就会来信息说,物件已寄出,后续还会收到已签收等信息。
那我们此时编程中,把信息发送出去了,可以打印个日志,看看谁发送了数据,发送的数据又是什么
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//接收来自客户端发送的数据
DatagramPacket packet = new DatagramPacket(new byte[4096], 4096);
socket.receive(packet);
//处理已发送的数据
String receiveData = new String(packet.getData(), 0, packet.getLength());
String SendData = Handle(receiveData);
//发送已处理的数据的
DatagramPacket send = new DatagramPacket(SendData.getBytes(), SendData.getBytes().length,
packet.getSocketAddress());
socket.send(send);
//打印日志
System.out.printf("[%s:%d],%s,%s\n", packet.getAddress(), packet.getPort(), receiveData, SendData);
}
}
private String Handle(String receiveData) {
return receiveData;
}
代码解释
DatagramPacket packet = new DatagramPacket(new byte[4096], 4096);
new byte[4096]:创建可以容纳4096个字节的数组。
4096:表示DatagramPacket缓冲区最大容量,意味着最多能接受4096的数据长度
当然值得注意的是,这个数字可以自己选择性的填。
DatagramPacket send = new DatagramPacket(SendData.getBytes(), SendData.getBytes().length, packet.getSocketAddress());
这里是包装要发送的数据
值得注意的的是SendData.getBytes().length,是获取字节数组的长度,而不是字符串的长度
这两个是有区别的
然后,既然是发送数据,那么肯定是要知道对方的IP地址和端口号
所以接收数据的时候,Packet已经保留好数据了
此时呢, packet.getSocketAddress()调用即可,这个方法返回了端口号和IP地址。
最后通过main方法来填入服务器的端口号即可
public static void main(String[] args) throws IOException {
UdpServer udpServer=new UdpServer(9096);
udpServer.start();
}
这个服务器代码算是写到这了,接下来写客户端的代码
客户端
客户端代码其实和上面的服务器核心流程也是差不多的,当然还是有些差别的。
首先,客户端这里,首先要输入自己的IP地址和服务器的端口号
就像是寄快递时候的寄件人地址和收件人地址。
咦,那为什么客户端这里不要指定本身的端口号呢?
这是因为,如若我们指定的端口号了,我们不知道此时的端口和电脑哪个进行使用的端口号一致了,即使是排查了,也是比较麻烦和耗费时间了,所以干脆让系统自动分配即可。
代码:
public class UdpClient {
DatagramSocket socket=null;
public String IP;
public int port;
public UdpClient(String IP,int port) throws SocketException {
this.socket = new DatagramSocket();
this.port = port;
this.IP = IP;
}
}
接着
就是客户端发送数据,这里呢,发送数据的形式,是让用户输入内容。
然后包装下客户端输入的数据内容
发送给服务器
服务器返回数据后,客户端处理返回的数据,当然,我们这里的是回显客户端,所以返回的数据可以不做任何处理,只需那一个DatagramPacket来接收即可。
然后就可以打印我们客户端处理过的数据了
代码:
public void start() throws IOException {
System.out.print("启动客户端!\n");
while (true){
System.out.println("客户端输入内容:");
//构造发送数据
Scanner scanner=new Scanner(System.in);
String SendData=scanner.next();
DatagramPacket packet=new DatagramPacket(SendData.getBytes(),SendData.getBytes().length,
InetAddress.getByName(this.IP),this.port);
socket.send(packet);
//处理服务器返回的数据
DatagramPacket response=new DatagramPacket(new byte[4096],4096);
socket.receive(response);
String responseData=new String(response.getData(),0,response.getLength());
//打印返回的数据
System.out.println("返回的数据:"+responseData);
}
}
代码解释:
DatagramPacket packet=new DatagramPacket(SendData.getBytes(),SendData.getBytes().length, InetAddress.getByName(this.IP),this.port);
既然是发送数据给到服务器,服务器既然要发送信息回到客户端,此时呢,客户端肯定是要把自己IP地址发送过去。
那为什么需要InetAddress.getByName()这里呢?
这是因为:
DatagramPacket构造方法中:
public DatagramPacket(byte[] buf, int length,
InetAddress address, int port) {
this(buf, 0, length, address, port);
}
address类型是InetAddress,我们输入的IP地址是字符串,所以需要转换一下。
String responseData=new String(response.getData(),0,response.getLength());
代表了,返回回来的数据,是从0下标,到整个字节数据的长度,即代表了服务器返回的所有内容。
值得注意的是,0下标位置代表的是偏移量,即从哪里开始向String填入数据。
最后,我们就通过Main方法,填入自身的IP地址和服务器的端口号即可
public static void main(String[] args) throws IOException {
UdpClient client=new UdpClient("127.0.0.1",9096);
client.start();
}
值得注意的是,这里填入的IP地址是回环地址,这是因为我们的程序是在本地上跑的。
回环地址:是计算机网络中用于本地回环接口的一个特殊IP地址。它允许设备向自身发送和接收网络数据,主要用于测试和调试目的。
测试效果:
那么对于基于UDP协议实现的服务器客户端,就讲到这。
接下来讲讲基于TCP协议实现的服务器和客户端
TCP
实现代码之前,首先我们就先来认识下JAVA对于TCP封装好的API吧
值得注意的是,TCP是面向字节流的,所以后面涉及到方法,也会涉及到字节流的API
ServerSocket
是TCP服务端Socket的API
构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket方法
方法签名 | 方法说明 |
Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端 连接后,返回⼀个服务端Socket对象,并基于该 Socket建⽴与客⼾端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
为什么需要accept()?这是TCP一个定义就是有连接的,所以操作客户端发送过来的数据之前,
首先要连接到客户端,连接成功,返回Socket对象,此时我们拿着这个对象,就可以对数据进行操作了。
值得注意的是,这个连接是内核已经处理好的,不需要程序员去关心了。
Socket
是客户端使用Socket。
不管是客户端还是服务端使用的Socket,都是双方建立连接后,保存对端的信息,及用来与对方收发数据的。
构造方法:
方法签名 | 方法说明 |
Socket(String host, int port) | 创建⼀个客⼾端流套接字Socket,并与对应IP的主机 上,对应端⼝的进程建⽴连接 |
方法:
方法签名 | 方法说明 |
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输⼊流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
现在呢,对服务端代码如何实现做出讲解。
服务器
首先,对于这个构造方法来说呢,其实UDP差不多的
都是要绑定一个端口号
代码:
public class TCPServer {
ServerSocket socket=null;
public TCPServer(int port) throws IOException {
socket=new ServerSocket(port);
}
}
接着到这个核心逻辑这边了
对于核心逻辑,其实在服务器代码中,是大差不差的
都是先接收客户端信息,然后对客户端信息做出相关操作,然后把数据返回到客户端。
当然这一切都要基于先连接到客户端先
上面的构造方法中,socket对象创建了,现在就要调用accpet()方法即可
代码:
public void start() throws IOException {
System.out.println("启动TCP服务器!");
while (true){
Socket clientSocket=socket.accept();
HandleClient(clientSocket);
}
}
由于服务器是7X24小时不断运行的,所以给到一个while是合理的,不断处理客户端传输过来的业务
接下来再讲讲这个HandleClient()处理逻辑了
在这个方法中,我们首先使用getOutputStream()、getInputStream()方法,
对客户端的数据进行读取,然后再写回客户端
当然如若是单纯使用getOutputStream()、getInputStream()方法会显得有些麻烦
毕竟要构造字节数组进行存储数据和读取数据。
所以这里使用Scanner、PrintWriter两个类对这个getOutputStream()、getInputStream()返回值进行包装。
代码:
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
PrintWriter printWriter=new PrintWriter(outputStream);
Scanner scanner=new Scanner(inputStream);
} catch (IOException e){
e.printStackTrace();
}
值得注意的是,PrintWriter是一个高级工具,它底层依赖于OutputStream,而OutputStream会调用系统提供的send()API(假设,方法名可能不一致)发送数据。
这里使用Scanner来读取数据
比如其中的方法next(),那么此时呢,就是读取到空格就停止了读取。
那么为了客户端也是可以这样子的读取数据,所以使用了PrintWriter提供的方法println(),
这个方法会先写到底层OutputStream的缓冲区中,然后在提供的数据中,最后添加一个换行符。
代码:
private void HandleSocket(Socket clientSocket) throws IOException {
//打印客户端信息
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
PrintWriter printWriter=new PrintWriter(outputStream);
Scanner scanner=new Scanner(inputStream);
//获取到客户端信息。
while (true){
if(!scanner.hasNext()){
break;
}
String receiveData=scanner.next();
//处理得到的信息
String sendData=process(receiveData);
//发送处理的信息到客户端
printWriter.println(sendData);
//刷新缓冲区
printWriter.flush();
//打印日志
System.out.printf("[%s:%d],%s,%s\n",clientSocket.getInetAddress(),clientSocket.getPort()
,receiveData,sendData);
}
}catch (IOException e){
e.printStackTrace();
}finally {
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
clientSocket.close();
}
}
private String process(String receiveData) {
return receiveData;
}
代码解释:
printWriter.flush();
前面说过,数据先写到底层缓冲区中,而缓冲区满了才会发送数据出去,所以可以调用flush()方法手动刷新缓冲区,使得数据提前发送出去
if(!scanner.hasNext()){
break;
}
这里的意思是,当我们读取数据的时候,没有数据可读,就要退出了,至于为什么没有数据
那么大概率就是被意外中断,或者输入流关闭了。
输入流关闭意味着客户端这边断开了连接,此时可以说明的是,客户端下线了。
clientSocket.close();
系统进行accept()的时候,那么此时会占用系统的文件描述符和内存资源。
内存资源好说,可以让JVM的垃圾回收兜底,但是文件文件描述符确是有限的,所以一旦长时间不用,而没有正确关闭,此时呢,会造成资源浪费。
当然此时的单线程环境,可以不用close(),但是为了养成习惯最好还是写上它
最后在main方法中填入要绑定的端口号即可
public static void main(String[] args) throws IOException {
TCPServer server=new TCPServer(9090);
server.start();
}
那么到这里,这个服务器代码就写完了。
接下来写客户端代码
客户端
此时构造方法的形式和UDP客户端是差不多的
都是先确定端口和IP地址
public class TCPClient {
Socket socket=null;
public TCPClient(String IP,int port) throws IOException {
socket=new Socket(IP,port);
}
}
接着核心逻辑更是和UDP那边大差不差了。
思路:
首先客户端那边先写入数据是吧,那么此时呢,写入数据用到了Scanner
然后把这个数据发送到服务器中,那么如何发送呢。
还是用到了PrintWriter printWriter=new PrintWriter(outputStream),这个类。
然后我们对服务器返回的数据进行读取,那么如何读取呢?
使用到了socket.getInputStream()。
代码:
public void start(){
System.out.println("客户端启动!");
//发送数据到服务器
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scanner=new Scanner(System.in);
Scanner scannerIn=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
System.out.println("客户端输入内容:");
String sendData=scanner.next();
printWriter.println(sendData);
printWriter.flush();
if(!scannerIn.hasNext()){
break;
}
//处理得到的数据
String receiveData=scannerIn.next();
//打印返回结果
System.out.println(receiveData);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
最后,我们在main方法中,填入IP地址和端口号即可
public static void main(String[] args) throws IOException {
TCPClient client=new TCPClient("127.0.0.1",9090);
client.start();
}
到这里,基于UDP/TCP协议的服务器和客户端算是完成了。
效果测试
源码:
UDP服务器:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpServer {
DatagramSocket socket = null;
public UdpServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//接收来自客户端发送的数据
DatagramPacket packet = new DatagramPacket(new byte[4096], 4096);
socket.receive(packet);
//处理已发送的数据
String receiveData = new String(packet.getData(), 0, packet.getLength());
String SendData = Handle(receiveData);
//发送已处理的数据的
DatagramPacket send = new DatagramPacket(SendData.getBytes(), SendData.getBytes().length,
packet.getSocketAddress());
socket.send(send);
//打印日志
System.out.printf("[%s:%d],%s,%s\n", packet.getAddress(), packet.getPort(), receiveData, SendData);
}
}
private String Handle(String receiveData) {
return receiveData;
}
public static void main(String[] args) throws IOException {
UdpServer udpServer=new UdpServer(9096);
udpServer.start();
}
}
UDP客户端:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpClient {
DatagramSocket socket=null;
public String IP;
public int port;
public UdpClient(String IP,int port) throws SocketException {
this.socket = new DatagramSocket();
this.port = port;
this.IP = IP;
}
public void start() throws IOException {
System.out.print("启动客户端!\n");
while (true){
System.out.println("客户端输入内容:");
//构造发送数据
Scanner scanner=new Scanner(System.in);
String SendData=scanner.next();
DatagramPacket packet=new DatagramPacket(SendData.getBytes(),SendData.getBytes().length,
InetAddress.getByName(this.IP),this.port);
socket.send(packet);
//处理服务器返回的数据
DatagramPacket response=new DatagramPacket(new byte[4096],4096);
socket.receive(response);
String responseData=new String(response.getData(),0,response.getLength());
//打印返回的数据
System.out.println("返回的数据:"+responseData);
}
}
public static void main(String[] args) throws IOException {
UdpClient client=new UdpClient("127.0.0.1",9096);
client.start();
}
}
TCP服务器:
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;
public class TCPServer {
ServerSocket socket=null;
public TCPServer(int port) throws IOException {
socket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动TCP服务器!");
while (true){
Socket clientSocket=socket.accept();
HandleClient(clientSocket);
}
}
private void HandleSocket(Socket clientSocket) throws IOException {
//打印客户端信息
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
PrintWriter printWriter=new PrintWriter(outputStream);
Scanner scanner=new Scanner(inputStream);
//获取到客户端信息。
while (true){
if(!scanner.hasNext()){
break;
}
String receiveData=scanner.next();
//处理得到的信息
String sendData=process(receiveData);
//发送处理的信息到客户端
printWriter.println(sendData);
//刷新缓冲区
printWriter.flush();
//打印日志
System.out.printf("[%s:%d],%s,%s\n",clientSocket.getInetAddress(),clientSocket.getPort()
,receiveData,sendData);
}
}catch (IOException e){
e.printStackTrace();
}finally {
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
clientSocket.close();
}
}
private String process(String receiveData) {
return receiveData;
}
public static void main(String[] args) throws IOException {
TCPServer server=new TCPServer(9090);
server.start();
}
}
TCP客户端:
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 TCPClient {
Socket socket=null;
public TCPClient(String IP,int port) throws IOException {
socket=new Socket(IP,port);
}
public void start(){
System.out.println("客户端启动!");
//发送数据到服务器
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
Scanner scanner=new Scanner(System.in);
Scanner scannerIn=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
System.out.println("客户端输入内容:");
String sendData=scanner.next();
printWriter.println(sendData);
printWriter.flush();
if(!scannerIn.hasNext()){
break;
}
//处理得到的数据
String receiveData=scannerIn.next();
//打印返回结果
System.out.println(receiveData);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPClient client=new TCPClient("127.0.0.1",9090);
client.start();
}
}
完!