网络协议通信
IP和端口号
要想使计算机能够通信,必需为每台计算机指定一个标识号,通过这个标识号指定接受数据的计算机或者发送数据的计算机。一般的,IP地址就是一个计算机的标识号,它可以唯一标识一台计算机。
IP地址由两部分组成:网络地址和主机地址。在IPv4中,IP地址是32位二进制数字,通常用四个十进制数表示,每个数之间用句点分隔,如192.168.0.1。其中前面的三个数代表网络地址,最后一个数代表主机地址。
在IPv4中,IP地址按照网络规模的不同可以分为5类,分别是A类、B类、C类、D类和E类。
A类地址:以0开头,第一个字节(即第一段地址)为网络地址,后三个字节(即第二段、第三段和第四段地址)为主机地址。A类地址范围从1.0.0.0到127.255.255.255,可以分配给大型网络。
B类地址:以10开头,前两个字节(即第一段和第二段地址)为网络地址,后两个字节(即第三段和第四段地址)为主机地址。B类地址范围从128.0.0.0到191.255.255.255,可以分配给中等规模的网络。
C类地址:以110开头,前三个字节(即第一段、第二段和第三段地址)为网络地址,最后一个字节(即第四段地址)为主机地址。C类地址范围从192.0.0.0到223.255.255.255,可以分配给小型网络。
D类地址:以1110开头,D类地址用于多点广播,范围从224.0.0.0到239.255.255.255。
E类地址:以1111开头,E类地址保留作实验和研究之用,范围从240.0.0.0到255.255.255.255。
另外还有一个回送地址127.0.0.1,指本机地址,一般用于测试。
通过IP地址可以连接到指定计算机,但如果需要访问目标计算机某个程序,还需要指定端口号。
端口号是一种用于标识计算机或设备上应用程序的网络地址的数字。在计算机网络中,每个应用程序都需要一个唯一的端口号来与其他应用程序通信。
常见的端口号包括:
HTTP(超文本传输协议)使用端口号80
HTTPS(加密的HTTP)使用端口号443
FTP(文件传输协议)使用端口号21
SSH(安全外壳协议)使用端口号22
Telnet(远程终端协议)使用端口号23
SMTP(简单邮件传输协议)使用端口号25
DNS(域名系统)使用端口号53
DHCP(动态主机配置协议)使用端口号67和68
POP3(邮局协议版本3)使用端口号110
IMAP(互联网消息访问协议)使用端口号143
端口号通常由16位数字组成,取值范围从0到65535。其中0到1023之间的端口号被保留用于系统级别的服务和应用程序,而1024到65535之间的端口号则可以被普通应用程序使用。
InetAddress
Java中的一个与IP相关的类,封装了IP地址,提供了一系列与IP地址相关的方法。
常用方法:
import java.net.InetAddress;
public class Net {
public static void main(String[] args) throws Exception {
//获取本地主机InetAddress对象
InetAddress localAddr=InetAddress.getLocalHost();
//通过主机名获取InetAddress对象
InetAddress remoteAddr=InetAddress.getByName("www.baidu.com");
System.out.println("本机的IP地址: "+localAddr.getHostAddress());
System.out.println("远程主机的IP地址: "+remoteAddr.getHostAddress());
System.out.println("在3秒内是否可到达远程主机: "+remoteAddr.isReachable(3000));
System.out.println("远程主机的名称: "+remoteAddr.getHostName());
}
}
控制台输出:
UDP与TCP
UDP(用户数据报协议)是互联网协议套件中的传输层协议之一,它是一种无连接的协议,不需要在数据传输前建立连接。UDP在传输数据时不提供可靠性、流量控制和拥塞控制等服务,但传输效率高,开销小,适合于一些对实时性要求高、但对数据可靠性要求不高的应用场景,如在线游戏、音视频传输、DNS查询等。
UDP的数据包称为用户数据报,它的数据包结构比较简单,包含以下几个字段:
源端口号(16位):标识发送方的端口号。
目的端口号(16位):标识接收方的端口号。
长度(16位):标识用户数据报的长度。
校验和(16位):用于检验用户数据报是否传输正确。
数据(可选):要传输的数据,长度可以为0到65535个字节。
UDP在传输数据时,不保证数据的可靠性和完整性,因此需要应用层自行处理这些问题。如果需要在UDP上实现可靠数据传输,需要在应用层上进行相应的处理,比如增加重传机制、数据校验和等。
TCP(Transmission Control Protocol,传输控制协议)是一种基于连接的、可靠的、面向流的传输层协议,它提供了端到端的数据传输服务。TCP协议是因特网协议族中最常用的协议之一,它保证了数据传输的可靠性,确保数据从源到目的地的正确性和完整性。
TCP协议的特点包括:
基于连接:通信前需要建立连接,通信结束后需要释放连接,保证数据的可靠性。
可靠性:通过序列号和确认号机制,确保数据传输的正确性和完整性,可靠地传输数据。
面向流:数据在传输过程中被看作是一个连续的字节流,而不是分散的数据包。
全双工:通信双方可以同时发送和接收数据。
拥塞控制:通过拥塞控制机制,保证网络的稳定性和可靠性。
数据段首部:TCP协议在每个数据段的首部添加了序列号、确认号、窗口大小等控制信息,以保证数据的可靠性和完整性。
TCP协议的应用广泛,例如网页浏览、电子邮件、文件传输、远程登录等。
三次握手:
UDP通信
UDP是面向无连接的协议,通信时不需要发送端和接受端不需要建立连接。UDP通信过程,就像货运公司在两个码头间发送货物,在码头发送和接受货物需要使用集装箱来装载货物。UDP通信同样需要,发送和接收数据需要被集装箱”打包“,Java提供了DatagramPacket类,运输货物只有“集装箱”不够还需要“码头”。为此Java还提供了DatagramSocket类。
DatagramPacket
构造方法与常用方法:
DatagramSocket
构造方法与常用方法:
UDP网络程序
实现UDP需要创建一个发送端程序和接收端程序。运行时,必须先让接收端程序先运行,才能避免发送端找不到接收端而造成数据丢失问题。
建立接收端:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
//接收端
public class receiver {
public static void main(String[] args)throws Exception {
byte[]buf=new byte[1024];//创建字节数组,用于接受数据
//定义DatagramSocket对象,设置监听端口号
DatagramSocket ds=new DatagramSocket(9949);
//设置一个DatagramPacket对象用于接受数据
DatagramPacket dp=new DatagramPacket(buf, buf.length);
System.out.println("等待接受数据..");
ds.receive(dp);//等待接受数据,没有数据则会都市
//调用DatagramPacket方法获得接收到的信息
//包括数据内容,长度,发送IP地址和端口号
String str=new String(dp.getData(),0,dp.getLength())+" From "+dp.getAddress().getHostAddress()+" : "+dp.getPort();
System.out.println(str);
ds.close();//释放资源
}
}
运行后控制台:
编写发送端:
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
//发送端
public class sender {
public static void main(String[] args) throws Exception{
//创建一个Socket对象
DatagramSocket ds=new DatagramSocket(8848);
String data="UDP数据包";//要发送的数据
byte[]arr=data.getBytes();//把定义字符串变为字节数组
//创建一个要发送的数据包,包括要发送的数据数据长度,接收端IP地址以及端口号
DatagramPacket dp=new DatagramPacket(arr,arr.length, InetAddress.getByName("localhost"),9949);
System.out.println("发送信息..");
ds.send(dp);//发送数据
ds.close();
}
}
再运行发送端:
发送端控制台:
接收端控制台:
(注意:设置端口号时如果发送端口号已被占用,只需关掉端口号或者新建一个没有使用的端口号即可)
多线程UDP网络程序
当接收端程序处于阻塞状态,运行发送端程序,接收端就会收到发送端发送端数据而结束阻塞状态,完成程序运行。实际上,发送端可以无限发送数据,接收端也可以一直接受数据,例如,聊天程序发送端可以一直发送消息,接收端也可以一直接受消息,发送端和客户端都是多线程的。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class threads_udp_demo {
public static void main(String[] args) {
new receice().start();
new send().start();
}
}
class receice extends Thread {
@Override
public void run() {
try {
//创建端口
DatagramSocket socket = new DatagramSocket(6666);
//创建数据包
DatagramPacket packet = new DatagramPacket(new byte[1024],1024);
while (true) {
socket.receive(packet);//接受数据
byte[]arr=packet.getData();
int len=packet.getLength();
String ip=packet.getAddress().getHostAddress();
System.out.println(ip+" : "+new String(arr,0,len)+" 端口: "+packet.getPort());
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
class send extends Thread {
@Override
public void run() {
try {
//创建端口
DatagramSocket socket = new DatagramSocket(9999);
Scanner sc=new Scanner(System.in);
while (true){
String data=sc.nextLine();
if("quit".equals(data)){
break;
}
DatagramPacket packet = new DatagramPacket(data.getBytes(),data.getBytes().length,
InetAddress.getByName("localhost"),6666);
socket.send(packet);
}
socket.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
模拟微信聊天
(1)通过上述任务描述可知此任务是使用多线程与UDP通信相关知识实现的。要实现聊天窗口界面。首先需要定义一个实现聊天功能的类,类中需要定义访问聊天的输出语句,从而获取输入的发送端端口号、接收端端口号以及实现发送和接收功能的方法。
import java.util.Scanner;
public class Room {
public static void main(String[] args) {
System.out.println("欢迎使用聊天功能!");
Scanner sc=new Scanner(System.in);
System.out.println("请输入您的账号:");
int sendPort=sc.nextInt();
System.out.println("请输入您要发送的账号:");
int receivePort=sc.nextInt();
System.out.println("系统启动..");
//发送操作
new Thread(new SendTask(sendPort),"发送端").start();
//接受操作
new Thread(new ReceiveTask(receivePort), "接收端").start();
}
}
(2)实现发送数据的功能。该功能通过一个实现了Runnable接口的类实现,类中需要定义获取发送数据的端口号,并在实现run()的方法中,编写发送数据的方法。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class SendTask implements Runnable{
private int sendPort;//发送数据端口号
//构造方法
public SendTask(int sendPort) {
this.sendPort = sendPort;
}
@Override
public void run() {
try {
//创建端口对象
DatagramSocket ds=new DatagramSocket();
//输入发送数据
Scanner sc=new Scanner(System.in);
while (true){
String data=sc.nextLine();
byte[]buf=data.getBytes();
DatagramPacket dp=new DatagramPacket(buf,buf.length,
InetAddress.getByName("127.0.0.255"),sendPort);
//发送数据
ds.send(dp);
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
(3)实现接收数据的功能。该功能通过一个实现了Runnable接口的类实现,类中需要定义获取接收数据的端口号,并在实现run()的方法中,编写显示接收到的数据的方法。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ReceiveTask implements Runnable{
private int receivePort;
public ReceiveTask(int receivePort) {
this.receivePort = receivePort;
}
@Override
public void run() {
try {
//创建端口
DatagramSocket ds=new DatagramSocket(receivePort);
//创建packet
byte[]buf=new byte[1024];
DatagramPacket dp=new DatagramPacket(buf, buf.length);
while (true){
ds.receive(dp);
//显示接收到数据
String str=new String(dp.getData(),0,dp.getLength());
System.out.println("收到 "+dp.getAddress().getHostAddress()+"" +
"发送的数据 : "+str);
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
(4)创建完所有的类与方法后,运行两次程序,同时开启两个窗口来实现聊天功能。
TCP通信
TCP通信两端都需要创建Scoket对象,严格区分客户端和服务端,通信时,必须先由客户端连接服务器端才能实现通信,服务器端不能主动连接客户端,而且服务器端必须事先启动,等待客户端的连接。
Java提供两个用于实现TCP程序的类,ServerSocket,用于标识服务器端;Socket,用于标识客户端。
通信时,首先创建服务器对象,然后等待客户端连接;然后创建客户端对象,开启服务,客户端对象向服务器端发起连接请求,服务器端响应后,两者才能建立连接,开始通信。
ServerSocket
构造方法和常用方法:
ServerSocket对象负责监听某台计算机的某个端口号,创建对象后,继续调用该对象的accept方法,接受来自客户端的请求。执行accept方法后,服务器程序发生阻塞,直到客户端发出连接请求,accept才会返回一个Socket对象用于与客户端实现通信,程序才能继续向下执行。
Socket
构造方法和常用方法:
简易TCP网络程序
首先实现服务端:
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args)throws Exception {
new TCPServer().listen();
}
}
class TCPServer{
private static final int PORT=7788;//定义端口号
public void listen()throws Exception {//定义一个listen方法抛出异常
ServerSocket ss=new ServerSocket(PORT);
//调用accept方法接受数据
Socket client=ss.accept();
OutputStream os=client.getOutputStream();//获取客户端输出流
System.out.println("开始与客户端交互数据..");
//当客户端连接到服务器,向客户端输出数据
os.write(("数据已传输").getBytes());
Thread.sleep(5000);//模拟其他功能占用时间
System.out.println("结束与客户端交互数据");
os.close();
client.close();
}
}
accept方法只有接收到了客户端访问才会停止阻塞。
客户端:
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
public class Client {
public static void main(String[] args)throws Exception {
new TCPClient().connect();
}
}
class TCPClient {
private static final int PORT=7788;//服务器端口号
public void connect()throws Exception {
//创建一个Socket连接指定计算机的端口号
Socket client=new Socket(InetAddress.getLocalHost(),PORT);
InputStream is=client.getInputStream();//得到服务器数据流
byte[]buf=new byte[1024];
int len=is.read(buf);//数据读取到缓冲区
System.out.println("服务器端发送端数据: ");
System.out.println(new String(buf,0,len));
client.close();//关闭Socket对象,释放资源
}
}
先运行服务器端,在运行客户端,查看控制台输出:
多线程TCP程序
在日常需求中,经常会有多个客户端访问一个服务端,服务端应给每个客户端都开启对应Socket建立专线通信。
对上述Server代码进行修改:
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args)throws Exception {
new TCPServer().listen();
}
}
class TCPServer{
private static final int PORT=7788;//定义端口号
public void listen()throws Exception {//定义一个listen方法抛出异常
ServerSocket ss=new ServerSocket(PORT);
//不断接受客户端请求
while (true){
//调用accept方法接受数据
Socket client=ss.accept();
new Thread(){
@Override
public void run() {
OutputStream os;
try {
os=client.getOutputStream();//获取客户端输出流
System.out.println("开始与客户端交互数据..");
//当客户端连接到服务器,向客户端输出数据
os.write(("数据已传输").getBytes());
Thread.sleep(5000);//模拟其他功能占用时间
System.out.println("结束与客户端交互数据");
}catch (Exception e) {
e.printStackTrace();
}
};
}.start();
}
}
}
运行,不断使用客户端,查看控制台输出:
反转字符串
编写一个小程序。实现客户端向服务器传递一个字符串(键盘录入) ,服务器(多线程)将字符串反转后写回,客户端再次接读取到的是反转后的字符串。要求使用多线程与TCP通信相关知识实现。
(1)根据任务描述可以知道该程序用TCP通信技术实现,所以第一条就是定义客户端,键盘录入数据定义Scanner来实现,然后创建客户端指定IP地址和端口号,之后获取输出流,与输入流,最后将字符串写到服务器并将反转后的结果读出来打印在控制台。
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws UnknownHostException,
IOException {
//创建键盘录如对象
Scanner sc = new Scanner(System.in);
//创建客户端,指定ip地址和端口号
Socket socket = new Socket("127.0.0.1", 8848);
BufferedReader br = new BufferedReader(new
InputStreamReader(socket.getInputStream())); //获取输入流
//获取输出流
PrintStream ps = new PrintStream(socket.getOutputStream());
//将字符串写到服务器去
ps.println(sc.nextLine());
System.out.println(br.readLine()); //将反转后的结果读出来
socket.close();
}
}
(2)实现服务端的代码编写,首先创建服务端绑定客户端的端口号,并用Server的accept()方法接受客户端的请求。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args)throws IOException {
ServerSocket ss = new ServerSocket(8848);
System.out.println("服务器启动,绑定8848端口.."); //不断接受客户端请求
while (true) {
//调用accept方法接受数据
final Socket socket = ss.accept();
new Thread() {
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));//获取输入流
PrintStream ps = new PrintStream(socket.getOutputStream());
String line = br.readLine();
line = new StringBuilder(line).reverse().toString();
ps.println(line);//写回
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
(3)服务端定义run()方法实现之后获取输入输出流,将客户端发送过来的数据读取出来并采用链式编程的思想将字符串反转后返回到客户端。
查看控制台数据:
文件上传
编写一个客户端向服务端上传文件的程序,要求使用TCP通信的的知识,完成将本地机器输入的路径下的文件上传到D盘中名称为upload的文件夹中。并把客户端的IP地址加上count标识作为上传后文件的文件名,即IP(count)的形式。其中,count随着文件的增多而增大,例如127.0.0.(1).jpg、127.0.0.(2).jpg。
(1)根据任务描述中使用TCP通信的知识实现文件上传功能可知,要实现此功能,需要定义一个服务器接收文件的程序和 一个客户端上传文件的程序。
(2)首先要编写服务器端程序来接收文件。服务器端需要使用ServerSocket对象的accept()方法接收客户端的请求,由于一个服务器可能对于多个客户端,所以当客户端与服务器端简历连接后,服务器需要单独开启一个新的线程来处理与客户端的交互,这时需要在服务器端编写开启新线程的方法。在新线程的方法中,需要获取客户端的端口号,并且使用输入输出流来传输文件到指定的目录中。
package tcp_demo;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class FileServer {
public static void main(String[] args) throws Exception {
//创建ServerSocket对象
ServerSocket serverSocket = new ServerSocket(10001);
while (true) {
// 调用accept()方法接收客户端请求,得到Socket对象
Socket s = serverSocket.accept();
// 每当和客户端建立Socket连接后,单独开启一个线程处理和客户端的交互
new Thread(new ServerThread(s)).start();
}
}
}
class ServerThread implements Runnable {
// 持有一个Socket类型的属性
private Socket socket;
// 构造方法中把Socket对象作为实参传入
public ServerThread(Socket socket) {
this.socket = socket;
}
public void run() {
// 获取客户端的IP地址
String ip = socket.getInetAddress().getHostAddress();
// 上传图片个数
int count = 1;
try {
InputStream in = socket.getInputStream();
// 创建上传图片目录的File对象
File parentFile = new File("D:\\upload\\");
// 如果不存在,就创建这个目录
if (!parentFile.exists()) {
parentFile.mkdir();
}
// 把客户端的IP地址作为上传文件的文件名
File file = new File(parentFile, ip + "(" + count +
").jpg");
while (file.exists()) {
// 如果文件名存在,则把count++
file = new File(parentFile, ip + "(" + (count++) +
").jpg");
}
// 创建FileOutputStream对象
FileOutputStream fos = new FileOutputStream(file);
// 定义一个字节数组
byte[] buf = new byte[1024];
// 定义一个int类型的变量len,初始值为0
int len = 0;
// 循环读取数据
while ((len = in.read(buf)) != -1) {
fos.write(buf, 0, len);
}
// 获取服务端的输出流
OutputStream out = socket.getOutputStream();
// 上传成功后向客户端写出“上传成功”
out.write("上传成功".getBytes());
// 关闭输出流对象
fos.close();
// 关闭Socket对象
socket.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
(3)编写客户端的功能代码,客户端功能的实现,因为是用户自己输入上传文件。所以要定义键盘录入。录入后需要使用Socket类来创建客户对象,并通过输入输出流来定义指定的文件。
package tcp_demo;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class FileClient {
public static void main(String[] args)throws Exception {
// 创建客户端Socket
Socket socket = new Socket("127.0.0.1", 10001);
// 获取Socket的输出流对象
OutputStream out = socket.getOutputStream();
// 创建FileInputStream对象
System.out.println("请输入你要上传文件的路径:");
Scanner sc =new Scanner(System.in);
String upload = sc.nextLine();
if(!upload.isEmpty()){
FileInputStream fis = new FileInputStream(upload);
// 定义一个字节数组
byte[] buf = new byte[1024];
// 定义一个int类型的变量len
int len;
// 循环读取数据
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
// 关闭客户端输出流
socket.shutdownOutput();
// 获取Socket的输入流对象
InputStream in = socket.getInputStream();
// 定义一个字节数组
byte[] bufMsg = new byte[1024];
// 接收服务端的信息
int num = in.read(bufMsg);
String Msg = new String(bufMsg, 0, num);
System.out.println(Msg); // 关键输入流对象
fis.close();
// 关闭Socket对象
socket.close();
}else {
System.out.println("请输入文件路径后再上传 !");
}
}
}
(4)最后我们启动程序,先启动服务端程序,再运行客户端程序来测试上传的结果。