题目:
课题内容10、编写一个简单的端口扫描程序
目的:熟悉Linux/Windows下socket、网络编程的基本方法;
任务:编写一个简单的程序,该程序可扫描局域网的某计算机开放了哪些端口
思考路线及实现
首先我们需要知道,如何完成一个简单的端口扫描系统?
在Java程序中,创建java.net.Socket包中的Socket,填入连接IP地址和端口,若抛出IOException错误,则表示连接失败,若IP地址无误,就是端口没开放。
System.out.print("输入你需要扫描的host:");
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
for (int i = 1; i <= 100; i++)
{
try
{
Socket socket = new Socket(s, i);
socket.close();
System.out.println("host:"+s+ " port: "+i+" is open");
}catch (IOException e)
{
System.out.println("host:"+s+ " port: "+i+" is close");
}
}
优化方案
根据网上别人写的端口扫描,学到通过InetSocketAddress类进行连接,这时候,不会对每个IP和端口真正的连接,而是使用“探测”的方式,设置超时时间为200ms,这样速度能提升很多(特别是扫描本机以外的IP地址)。
代码实现:
System.out.print("输入你需要扫描的host:");
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
for (int i = 1; i <= 100; i++)
{
try
{
Socket socket = new Socket();
socket.connect(new InetSocketAddress(s,i),200);
socket.close();
System.out.println("host:"+s+ " port: "+i+" is open");
}catch (IOException e)
{
System.out.println("host:"+s+ " port: "+i+" is close");
}
}
使用多线程优化
本次优化使用线程池,首先创建一个多线程类,实现Runnable接口。
public class 优快云Runnable implements Runnable{
public String host;
public int port;
public 优快云Runnable(String host, int port) {
this.host = host;
this.port = port;
}
@Override
public void run() {
if (TCPisPortOpen(host,port))
{
String message = "Port " + port + " is open";
System.out.println(message);
}
else
{
String message = "Port " + port + " is closed";
System.out.println(message);
}
}
private static boolean TCPisPortOpen(String host, int port) {
try {
// 创建Socket并尝试连接
Socket socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 200); // 200毫秒超时
socket.close();
return true;
} catch (IOException e) {
return false;
}
}
}
之后使用线程池实现
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);//开启多少线程
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池属性
service1.setMaximumPoolSize(500);//设置线程池中线程数的上限
for (int port = 1; port <= 100; port++) {
service.execute(new 优快云Runnable("127.0.0.1",port));
}
System.out.println("成功调用");
//关闭线程池
service.shutdown();
配合web使用
首先规划好自己想要的效果:我想要把扫描结果实时显示出来,这样对于用户体验感也好。基于这个需求我web需要一个框填写需要扫描的信息,一个框显示扫描结果的信息。这样一个大框架就想象出来了。
简单的web设计:
相关代码:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>扫描端口程序</title>
<script src="../static/js/axios.js"></script>
<style>
.box{
position: absolute;
width: 600px;
height: 400px;
top: 100px;
left: 150px;
background-color: aquamarine;
text-align: center;
z-index: 10;
}
.result{
position: absolute;
width: 600px;
height: 400px;
top: 100px;
right: 150px;
background-color: blanchedalmond;
text-align: center;
overflow-y: scroll;
z-index: 1;
}
.textOpen{
color: green;
}
.textClose{
color: red;
}
.select-box {
position: absolute;
top: 60px;
right: 150px;
}
.show-box {
position: absolute;
top: 60px;
right: 475px;
}
.error{
border: 2px solid red;
}
.error-host-text{
position: absolute;
top: 85px;
left: 153px;
color: red;
}
.error-thread-text{
position: absolute;
top: 150px;
left: 153px;
color: red;
}
.error-FPort-text{
position: absolute;
top: 215px;
left: 136px;
color: red;
}
.error-LPort-text{
position: absolute;
top: 279px;
left: 70px;
color: red;
}
.error-Link-text{
position: absolute;
top: 370px;
left: 160px;
color: red;
}
</style>
</head>
<body>
<script>
var total = []
var isOKHost = 0
var isOKThread = 0
var isOKFPort = 0
var isOKLPort = 0
var isOKLink = 0
let LTo;
const startLink = ()=>
{
//获取button按钮并改变
document.getElementById("link_button").disabled = true;
document.getElementById("link_button").innerHTML = "已经连接";
//获取result_box,以便添加html
const resultBox = document.getElementById("result");
//建立socket连接
const socket = new WebSocket("ws://localhost:8080/portScan")
//当连接成功时调用的方法
socket.onopen = () => {
window.alert("WebSocket连接已打开")
isOKLink = 1;
};
//监听信息时的方法
socket.onmessage = (event) => {
console.log("收到消息:", event.data);
const isOpen = event.data.split(" ");
//加入数组
total[isOpen[1]] = event.data
//创建一个div
const newDiv = document.createElement("div");
newDiv.innerHTML = event.data + "<br/>"
if (isOpen[3] === 'closed') {
newDiv.className = "textClose";
} else {
newDiv.className = "textOpen";
}
resultBox.appendChild(newDiv);
console.log("LTo = "+LTo + " total= "+total.length)
if (LTo === total.length)
{
document.getElementById("start_button").disabled = false;
document.getElementById("start_button").innerHTML = "点击开始";
alert("扫描完成")
}
};
//关闭连接时调用的方法
socket.onclose = (event) => {
console.log("WebSocket连接已关闭", event);
};
//连接出错时调用的方法
socket.onerror = (error) => {
console.error("WebSocket错误:", error);
};
}
//开始扫描
const scanstart = ()=>{
//获取用户输入信息
const host = document.querySelector('input[name="host"]').value;
const thread = document.querySelector('input[name="thread"]').value;
const FPort = document.querySelector('input[name="FPort"]').value;
const LPort = document.querySelector('input[name="LPort"]').value;
//检查所有的输入数是否合法
if ( isOKLink !==1 && isOKHost !==1 && isOKThread !==1 && isOKLPort !== 1 && isOKFPort !== 1)
{
//拒绝发出请求并进行提示
const box = document.getElementById("box")
//创建一个div
const newDiv = document.createElement("div");
newDiv.innerHTML = "您尚未连接或数据输入错误,请检查重来"
newDiv.className = "error-Link-text"
newDiv.id = "error-Link-text"
box.appendChild(newDiv);
}
else {
if (document.getElementById("error-Link-text") !==null) {
//先清空错误提示
const errorLink = document.getElementById("error-Link-text")
errorLink.innerHTML = ""
}
LTo = parseInt(LPort)
//先清空resultBox
const resultBox = document.getElementById("result");
resultBox.innerHTML = "";
//获取用户选择什么模式
const type = document.getElementById("options").value;
//清空结果数组
total.length = 0;
//将按钮设置为正在获取
//获取button按钮并设置为不可用
document.getElementById("start_button").disabled = true;
document.getElementById("start_button").innerHTML = "正在查询";
//发送请求获取
var xhr = new XMLHttpRequest();
//设置请求方法和URL
var url = "http://localhost:8080/scan/" + host + "/" + FPort + "/" + LPort + "/" + type + "/" + thread;
xhr.open('GET', url);
// 发送请求
xhr.send();
//设置请求完成后的回调函数
xhr.onreadystatechange = function () {
if (xhr.status >= 200 && xhr.status < 300) {
// 请求成功,处理响应数据
//获取button按钮并设置为不可用
} else {
// 请求失败,处理错误
alert('API请求失败,状态码:' + xhr.status);
}
};
}
}
//开始查找,因为数据有序且数据量不大,使用二分查找
const selectPort = ()=>{
//获取input的值
const StringSelectValue = document.querySelector('input[name="select-resultBox"]').value;
const selectValue = parseInt(StringSelectValue)
//使用二分查找法查数组
let left = 0;
let right = total.length-1;
console.log("right="+right)
while(left <= right)
{
const mid = Math.floor((left + right) / 2);
console.log("mid="+mid)
//处理数组
const SignTotal = total[mid].split(" ");
const StringPort = SignTotal[1]
const port = parseInt(StringPort)
console.log("left="+left+" mid="+mid+" right="+right+" port="+port + " selectValue="+ selectValue)
if (port === selectValue)
{
//清空box
const resultBox = document.getElementById("select-showBox");
resultBox.innerHTML = "";
//创建新的div
//创建一个div
const newDiv = document.createElement("div");
newDiv.innerHTML = total[mid]
if (SignTotal[3] === 'closed')
{
newDiv.className = "textClose";
}
else
{
newDiv.className = "textOpen";
}
resultBox.appendChild(newDiv);
return
}
else if (port < selectValue)
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
//查找失败
alert("查找失败,您扫描的范围和条件可能不包含该端口")
}
//开始排序
const sortList = ()=>{
//首先清空
const resultBox = document.getElementById("result");
resultBox.innerHTML = "";
//遍历数组
for (let i = 1; i <total.length; i++)
{
//分解
const type = total[i].split(" ")[3]
const newDiv = document.createElement("div");
newDiv.innerHTML = total[i];
if (type === 'closed')
{
newDiv.className = "textClose";
}
else
{
newDiv.className = "textOpen";
}
resultBox.appendChild(newDiv);
}
alert("排序完成")
}
//开始验证
//验证域名
function isHost(){
const host = document.querySelector('input[name="host"]').value;
// 定义IP地址和域名的正则表达式
var ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
var domainRegex = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// 验证输入值
var isValid = ipRegex.test(host) || domainRegex.test(host);
var inputField = document.getElementById("host")
var box = document.getElementById("box")
if (isValid) {
inputField.classList.remove('error');
const errorHost = document.getElementById("error-host-text")
errorHost.innerHTML = ""
isOKHost =1
} else {
inputField.classList.add('error');
//创建一个div
const newDiv = document.createElement("div");
newDiv.innerHTML = "您的ip地址或域名输入错误,请重新输入"
newDiv.className = "error-host-text"
newDiv.id = "error-host-text"
box.appendChild(newDiv);
isOKHost = 0
}
}
//验证线程输入值是否合法
function isThread(){
const thread = document.querySelector('input[name="thread"]').value;
//将输入值转化为数字
var numberValue = parseInt(thread);
//指定最大值
var max = 500;
//验证输入值是否是数字且大于0,小于max
var isValid = !isNaN(numberValue) && numberValue > 0 && numberValue < max;
var inputField = document.getElementById("thread")
var box = document.getElementById("box")
if (isValid) {
inputField.classList.remove('error');
const errorThread = document.getElementById("error-thread-text")
errorThread.innerHTML = ""
isOKThread = 1
} else {
inputField.classList.add('error');
//创建一个div
const newDiv = document.createElement("div");
newDiv.innerHTML = "您输入的线程不合规定,请重新输入"
newDiv.className = "error-thread-text"
newDiv.id = "error-thread-text"
box.appendChild(newDiv);
isOKThread = 0
}
}
//验证端口输入值是否合法
function isFPort(){
const FPort = document.querySelector('input[name="FPort"]').value;
//转换成数字
var portNumber = parseInt(FPort);
//验证是否是数字且在合法范围内
var isValid = !isNaN(portNumber) && portNumber >=0 && portNumber <=65535
//验证
var inputField = document.getElementById("FPort")
var box = document.getElementById("box")
if (isValid) {
inputField.classList.remove('error');
const errorFPort = document.getElementById("error-FPort-text")
errorFPort.innerHTML = ""
isOKFPort = 1
} else {
inputField.classList.add('error');
//创建一个div
const newDiv = document.createElement("div");
newDiv.innerHTML = "您输入的端口不合规定,请重新输入(0~65535)"
newDiv.className = "error-FPort-text"
newDiv.id = "error-FPort-text"
box.appendChild(newDiv);
isOKFPort = 0
}
}
//验证端口输入值是否合法
function isLPort(){
const FPort = document.querySelector('input[name="FPort"]').value;
const LPort = document.querySelector('input[name="LPort"]').value;
//两个变成数字
var FPortNum = parseInt(FPort)
var LPortNum = parseInt(LPort)
//验证是否是数字,是否合法且大于等于开始端口
var isValid = !isNaN(FPortNum) && !isNaN(LPortNum) && LPortNum >=0 && LPortNum <=65535 && LPortNum>=FPortNum
//验证
var inputField = document.getElementById("LPort")
var box = document.getElementById("box")
if (isValid) {
inputField.classList.remove('error');
const errorLPort = document.getElementById("error-LPort-text")
errorLPort.innerHTML = ""
isOKLPort = 1
} else {
inputField.classList.add('error');
//创建一个div
const newDiv = document.createElement("div");
newDiv.innerHTML = "您输入的端口不合规定,请重新输入(0~65535且大于等于开始端口)"
newDiv.className = "error-LPort-text"
newDiv.id = "error-LPort-text"
box.appendChild(newDiv);
isOKLPort = 0
}
}
</script>
<div class="box" id="box">
<br/>
<br/>
请输入需要扫描端口的ip地址(本地为localhost)
<br/>
<input type="text" id="host" name= "host" placeholder="需要监听的ip地址" onblur="isHost()"/>
<br/>
<br/>
请输入需要开启多少线程进行扫描(最大不超过500个)
<br/>
<input type="text" id="thread" name= "thread" value="1" placeholder="输入开启多少线程" onblur="isThread()"/>
<br/>
<br/>
请输入从哪个端口开始扫描
<br/>
<input type="text" id="FPort" name= "FPort" placeholder="输入开始端口" onblur="isFPort()"/>
<br/>
<br/>
请输入到哪个端口结束扫描
<br/>
<input type="text" id="LPort" name= "LPort" placeholder="输入结束端口" onblur="isLPort()"/>
<br/>
<br/>
<label for="options">请选择一个显示选项</label>
<select id="options" name="options">
<option value="All" selected>显示所有端口</option>
<option value="open">显示开启端口</option>
<option value="close">显示关闭端口</option>
</select>
<br/>
<br/>
<button id="link_button" type="button" onclick="startLink()">点击连接</button>
<button id="start_button" type="button" onclick="scanstart()">点击开始</button>
</div>
<div class="select-box">
<input type="text" name="select-resultBox" id="select-resultBox" placeholder="请输入查询的端口号">
<button type="button" id="select-button" onclick="selectPort()">点击查询</button>
<button type="button" id="sort-button" onclick="sortList()">点击排序</button>
</div>
<div id="select-showBox" class="show-box"></div>
<div class="result" id="result">
</div>
</body>
</html>
通过HTML我们可以看到,我们这次使用websocket进行实现,接下来我们进行websocket的实现(关于这个已经有很多博客有很详细的回答了,这里就不赘述了)。
首先导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
进行配置
@Configuration
public class webSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
写webSocket类,因为这是个面对老师演示的课程设计,所以所有扫描信息直接对所有用户进行广播。
@ServerEndpoint(value = "/portScan")
@Component
public class ScannerController {
/**
* 类的日志
*/
private static final Logger log = LoggerFactory.getLogger(ScannerController.class);
/**
* 为记录用户
*/
private static final CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();
/**
* 成功建立连接调用的方法
* */
@OnOpen
public void onOpen(Session session
) {
sessions.add(session);
log.info(session.getId()+"开启连接");
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session)
{
sessions.remove(session);
log.info(session.getId()+"关闭连接");
}
//接收到信息时对所有用户进行广播
public static synchronized void broadcast(String message) {
for (Session session : sessions) {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
最后写controller,说实话这个接口写的不优雅。
@RestController
public class MainController {
/**
* 获取host里的FPort-LPort端口所有状态
* @param host
* @param FPort
* @param LPort
* @return
*/
@GetMapping("/scan/{host}/{FPort}/{LPort}/{type}/{thread}")
public String Scan(@PathVariable("host") String host,
@PathVariable("FPort") String FPort,
@PathVariable("LPort") String LPort,
@PathVariable("type") String type,
@PathVariable("thread") String thread) {
int startPort = Integer.parseInt(FPort);
int endPort = Integer.parseInt(LPort);
int threads = Integer.parseInt(thread);
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(threads);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池属性
service1.setMaximumPoolSize(500);//设置线程池中线程数的上限
for (int port = startPort; port <= endPort; port++) {
service.execute(new RunnableTest(host,port,type));
}
System.out.println("成功调用");
//关闭线程池
service.shutdown();
return "请求成功";
}
}
这样就完成了,最后我会上传我的实验源码。
https://download.youkuaiyun.com/download/weixin_59915237/88979124?spm=1001.2014.3001.5501