前言
是一种面向连接的、可靠的、基于字节流的传输层通信协议
// server.js
const net = require('net')
const server = net.createServer()
server.on('connection', clientSocket => {
console.log('客户端连接成功')
// 监听客户端发送的数据
clientSocket.on('data', data => {
console.log(data.toString())
})
// 给当前连接的客户端发送数据
clientSocket.write('hello')
})
server.listen(3000)
// client.js
const net = require('net')
// 连接服务器
const client = net.createConnection({
host: '127.0.0.1',
port: 3000
})
client.on('connect', () => {
console.log('成功连接服务器')
// 给服务端发消息
client.write('world !')
})
// 监听服务器发送的数据
client.on('data', data => {
console.log(data.toString())
})
// server.js
const net = require('net')
const server = net.createServer()
// 客户端数组
const clients = []
server.on('connection', clientSocket => {
clients.push(clientSocket)
// 监听客户端发送的数据
clientSocket.on('data', data => {
clients.forEach(socket => {
// 排除自己,给所有连上的客户端发送消息
if (socket !== clientSocket) {
clientSocket.write('有人说:' + data)
}
})
})
// 监听客户端断开连接
clientSocket.on('end', () => {
const index = clients.findIndex(client => client === clientSocket)
clients.splice(index, 1)
})
})
server.listen(3000)
// client.js
const net = require('net')
const client = net.createConnection({
host: '127.0.0.1',
port: 3000
})
client.on('connect', () => {
// 监听终端的输入
process.stdin.on('data', data => {
client.write(data.toString().trim())
})
})
// 监听服务器发送的数据
client.on('data', data => {
console.log(data.toString())
})
UDP | TCP | |
---|---|---|
连接 | 无连接 | 面向连接 |
速度 | 无需建立连接,速度较快 | 需要建立连接,速度较慢 |
目的主机 | 一对一,一对多 | 仅能一对一 |
带宽 | UDP 报头较短,消耗带宽更少 | 消耗更多的带宽 |
消息边界 | 有 | 无 |
可靠性 | 低 | 高 |
顺序 | 无序 | 有序 |
注意:UDP协议的这种乱序性基本上很少出现,通常只会在网络非常拥挤的情况下才有可能发生
什么时候用 TCP,什么时候用 UDP?
单播是目的地址为单一目标的一种传播方式,地址范围:0.0.0.0 ~ 223.255.255.255
// server.js
const dgram = require('dgram')
const server = dgram.createSocket('udp4')
// 客户端连接成功触发
server.on('listening', () => {
const address = server.address()
console.log(`server running ${address.address}:${address.port}`)
})
// 客户端发送消息时触发
server.on('message', (msg, remoteInfo) => {
console.log(`${remoteInfo.address}:${remoteInfo.port} 发送消息: ${msg}`)
server.send('world', remoteInfo.port, remoteInfo.address)
})
// 服务器异常时触发
server.on('error', err => {
console.log('server error', err)
})
// 绑定端口
server.bind(3000)
// client.js
const dgram = require('dgram')
const client = dgram.createSocket('udp4')
// 服务器连接成功触发
client.on('listening', () => {
const address = client.address()
console.log(`client running ${address.address}:${address.port}`)
client.send('hello', 3000, 'localhost')
})
// 服务器发送消息时触发
client.on('message', (msg, remoteInfo) => {
console.log(`${remoteInfo.address}:${remoteInfo.port} 发送消息: ${msg}`)
})
// 客户端异常时触发
client.on('error', err => {
console.log('client error', err)
})
// 绑定端口
client.bind(8000)
255.255.255.255
192.168.10.255
// server.js
const dgram = require('dgram')
const server = dgram.createSocket('udp4')
// 客户端连接成功触发
server.on('listening', () => {
const address = server.address()
console.log(`server running ${address.address}:${address.port}`)
server.setBroadcast(true) // 开启广播模式
// server.setBroadcast(false) // 关闭广播模式
server.send('hello', 8000, '255.255.255.255')
// 每隔2s发送一条广播消息
setInterval(() => {
// 直接地址:192.168.10.255
// 受限地址:255.255.255.255
server.send('hello', 8000, '255.255.255.255')
}, 2000)
})
// 客户端发送消息时触发
server.on('message', (msg, remoteInfo) => {
console.log(`${remoteInfo.address}:${remoteInfo.port} 发送消息: ${msg}`)
server.send('world', remoteInfo.port, remoteInfo.address)
})
// 服务器异常时触发
server.on('error', err => {
console.log('server error', err)
})
// 绑定端口
server.bind(3000)
TCP 和 UDP 都属于网络传输层协议,如果要构建高效的网络应用,就应该从传输层进行着手。但是对于经典的浏览器网页和服务端通信场景,如果单纯的使用更底层的传输层协议则会变得麻烦。
所以对于经典的B(Browser)S(Server)通信,基于传输层之上专门制定了更上一层的通信协议:HTTP,用于浏览器和服务端进行通信。由于 HTTP 协议本身并不考虑数据如何传输及其他细节问题,所以属于应用层协议。
Server 实例
API | 说明 |
---|---|
Event:‘close’ | 服务关闭时触发 |
Event:‘request’ | 收到请求消息时触发 |
server.close() | 关闭服务 |
server.listening | 获取服务状态 |
请求对象
API | 说明 |
---|---|
request.method | 请求方法 |
request.url | 请求路径 |
request.headers | 请求头 |
request.httpVersion | 请求HTTP协议版本 |
响应对象
API | 说明 |
---|---|
response.end() | 结束响应 |
response.setHeader(name, value) | 设置响应头 |
response.removeHeader(name, value) | 删除响应头 |
response.statusCode | 设置响应状态码 |
response.statusMessage | 设置响应状态短语 |
response.write() | 写入响应数据 |
response.writeHead() | 写入响应头 |
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('Hello World\n')
})
server.listen(3000, '127.0.0.1', () => {
console.log('server running')
})
npm i mime
const http = require('http')
const fs = require('fs')
const path = require('path')
const mime = require('mime')
const server = http.createServer()
server.on('request', (req, res) => {
const url = req.url
// 统一处理静态资源
fs.readFile(url, (err, data) => {
if (err) {
throw err
}
const contentType = mime.getType(path.extname(url))
res.setHeader('Content-Type', contentType)
res.end(data)
})
})
server.listen(3000)
当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下六个循环阶段,nodejs事件循环和浏览器的事件循环完全不一样。
timers(定时器) : 此阶段执行那些由 setTimeout()
和 setInterval()
调度的回调函数.
I/O callbacks(I/O回调) : 此阶段会执行几乎所有的回调函数, 除了 close callbacks(关闭回调) 和 那些由 timers 与 setImmediate()
调度的回调.
idle(空转), prepare : 此阶段只在内部使用
poll(轮询) : 检索新的I/O事件; 在恰当的时候Node会阻塞在这个阶段
check(检查) : setImmediate()
设置的回调会在此阶段被调用
close callbacks(关闭事件的回调): 诸如 socket.on('close', ...)
此类的回调在此阶段被调用
在事件循环的每次运行之间, Node.js会检查它是否在等待异步I/O或定时器, 如果没有的话就会自动关闭.
如果event loop进入了 poll 阶段,且代码未设定timer,将会发生下面情况:
- 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
- 如果poll queue为空,将会发生下面情况:
- 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
- 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue,一旦到达就立即执行
如果event loop进入了 poll阶段,且代码设定了timer:
- event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.
进程是资源分配的最小单位,线程是CPU调度的最小单位
“进程——资源分配的最小单位,线程——程序执行的最小单位”
一个进程下面的线程是可以去通信的,共享资源
线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成,线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉。
谷歌浏览器
node服务
1)需要频繁创建销毁的优先用线程
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
2)需要进行大量计算的优先使用线程
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。这种原则最常见的是图像处理、算法处理。
3)强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
4)可能要扩展到多机分布的用进程,多核分布的用线程
5)都满足需求的情况下,用你最熟悉、最拿手的方式
总结: 线程快而进程可靠性高。
const cluster = require('cluster')
const http = require('http')
const cpus = require('os').cpus().length // 获取CPU的个数
if (cluster.isMaster) { // 主线程
for (let i = 0; i < cpus; i++) {
cluster.fork()
}
} else { // 子线程
http.createServer((req, res) => {
res.end('Hello World!')
}).listen(3000, () => {
console.log('Server Run 3000')
})
}
process 对象是 Node 的一个全局对象,提供当前 Node 进程的信息,他可以在脚本的任意位置使用,不必通过 require 命令加载
属性
方法
事件
beforeExit 事件,在 Node 清空了 EventLoop 之后,再没有任何待处理任务时触发,可以在这里再部署一些任务,使得 Node 进程不退出,显示的终止程序时(process.exit()),不会触发
exit 事件,当前进程退出时触发,回调函数中只允许同步操作,因为执行完回调后,进程退出
uncaughtException 事件,当前进程抛出一个没有捕获的错误时触发,可以用它在进程结束前进行一些已分配资源的同步清理操作,尝试用它来恢复应用的正常运行的操作是不安全的
process.on('uncaughtException', err => {
console.log(err)
})
warning 事件,任何 Node.js 发出的进程警告,都会触发此事件
爬虫的基本工作流程如下:
注意:在爬取目标网站之前,建议浏览该网站的robots.txt,来确保自己爬取的数据在对方允许范围之内
以http://web.itheima.com/teacher.html
网站目标为例,最终目的是下载网站中所有老师的照片
下载所有老师的照片,需要通过如下步骤实现:
const http = require('http')
const req = http.request('http://web.itheima.com/teacher.html', res => {
let data = ''
// 监听 data 事件,获取传递过来的数据片段
res.on('data', chunk => {
data += chunk
})
// 监听 end 事件,获取数据完毕时触发
res.on('end', () => {
console.log(data)
})
})
req.end()
可以通过jQuery的api来获取DOM元素中的属性和内容
npm i cheerio
// 官方 DEMO
const cheerio = require('cheerio')
const $ = cheerio.load('Hello world
')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
//=> Hello there!
const http = require('http')
const cheerio = require('cheerio')
const HOST = 'http://web.itheima.com'
const req = http.request(HOST + '/teacher.html', res => {
let data = ''
// 监听 data 事件,获取传递过来的数据片段
res.on('data', chunk => {
data += chunk
})
// 监听 end 事件,获取数据完毕时触发
res.on('end', () => {
const $ = cheerio.load(data)
// 获取所有图片
const imgs = Array.prototype.map.call($('.tea_main .tea_con li > img'), item => HOST + $(item).attr('src'))
console.log(imgs)
})
})
req.end()
npm i download
const http = require('http')
const cheerio = require('cheerio')
const download = require('download')
const HOST = 'www.xxx.com/'
const req = http.request(HOST + 'teacher.html', res => {
let data = ''
// 监听 data 事件,获取传递过来的数据片段
res.on('data', chunk => {
data += chunk
})
// 监听 end 事件,获取数据完毕时触发
res.on('end', () => {
const $ = cheerio.load(data)
// 获取所有图片,图片路径需转码
const imgs = Array.prototype.map.call($('.tea_main .tea_con li > img'), item => encodeURI(HOST + $(item).attr('src')))
// 批量下载图片到当前目录下 dist 文件夹下
Promise.all(imgs.map(x => download(x, 'dist'))).then(() => {
console.log('files downloaded!')
})
})
})
req.end()
执行tsc --init
初始化项目,生成ts配置文件
TS配置:
{
"compilerOptions": {
/* Basic Options */
"target": "es2015",
"module": "commonjs",
"outDir": "./bin",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
父类
// 父类 Spider
const http = require('http')
interface SpiderOptions {
url: string,
method?: string,
headers?: object
}
// 定义抽象类
export default abstract class Spider {
// 定义成员
options: SpiderOptions
// 初始化
constructor(options: SpiderOptions) {
this.options = options
this.start()
}
start() {
const { url, method = 'get', headers } = this.options
const req = http.request(url, {
method,
headers
}, (res: any) => {
let result: string = ''
// 监听 data 事件,获取传递过来的数据片段
res.on('data', (chunk: any) => {
result += chunk
})
// 监听 end 事件,获取数据完毕时触发
res.on('end', () => {
// 调用抽象方法,具体实现由子子孙孙继承实现
this.onCatchHTML(result)
})
})
req.end()
}
abstract onCatchHTML(result: string): any
}
子类
// 子类 Photos
const cheerio = require('cheerio')
const download = require('download')
const HOST = ''
// 继承 Spider,实现onCatchHTML方法
import Spider from './Spider'
export default class Photos extends Spider {
onCatchHTML(result: string) {
const $ = cheerio.load(result)
// 获取所有图片,图片路径需转码
const imgs = Array.prototype.map.call($('.tea_main .tea_con li > img'), item => encodeURI(HOST + $(item).attr('src')))
// 批量下载图片到当前目录下 dist 文件夹下
Promise.all(imgs.map(x => download(x, 'dist'))).then(() => {
console.log('files downloaded!')
})
}
}
测试
import Photos from './Photos'
new Photos({
url: 'http://api/teacher.html'
})
Selenium是一个Web应用的自动化测试框架,可以创建回归测试来检验软件功能和用户需求,通过框架可以编写代码来启动浏览器进行自动化测试,换言之,用于做爬虫就可以使用代码启动浏览器,让真正的浏览器去打开网页,然后去网页中获取想要的信息!从而实现真正意义上无惧反爬虫手段!
浏览器 | webdriver |
---|---|
Chrome | chromedriver(.exe) |
Internet Explorer | IEDriverServer.exe |
Edge | MicrosoftWebDriver.msi |
Firefox | geckodriver(.exe) |
Safari | safaridriver |
根据浏览器选择版本和平台(选浏览器版本号前三位一致的):
下载后放入项目根目录
npm i selenium-webdriver
const { Builder, By, Key, until } = require('selenium-webdriver')
;(async function example() {
// 打开chrome浏览器
const driver = await new Builder().forBrowser('chrome').build()
try {
// 自动打百度
await driver.get('https://www.baidu.com')
// 找到百度的 id 为 kw 的元素(也就是搜索框元素), 自动输入'淘宝'并回车
await driver.findElement(By.id('kw')).sendKeys('淘宝', Key.ENTER)
// 等 1s 修改网站 title
console.log(await driver.wait(until.titleIs('淘宝_百度搜索'), 1000))
} finally {
// 关闭浏览器
// await driver.quit()
}
})()
核心对象:
辅助对象:
用于构建WebDriver对象的构造器
const driver = new webdriver.Builder()
.forBrowser('chrome')
.build()
其他API如下:
可以获取或设置一些Options
如需设置Chrome的Options,需要先导入Options:
const { Options } = require('selenium-webdriver/chrome')
const options = new Options()
options.addArguments('Cookie=user_trace_token=20191130095945-889e634a-a79b-4b61-9ced-996eca44b107; X_HTTP_TOKEN=7470c50044327b9a2af2946eaad67653;')
通过构造器创建好WebDriver后就可以使用API查找网页元素和获取信息了:
建立连接:
WebSocket 连接必须由浏览器发起,因为请求协议是一个标准的 HTTP 请求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-webSocket-Key: client-random-string
Sec-webSocket-Version: 13
该请求和普通的HTTP请求有几点不同:
如果服务器接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching Protoclos
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
响应码 101 表示本次连接的HTTP协议即将被更改,更改后的协议就是 Upgrade: websocket指定的WebSocket协议
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws
(如果加密,则为wss
),服务器网址就是 URL。
最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
npm i ws
// client.js
const WebSocket = require('ws')
// 连接socket服务器
const ws = new WebSocket('ws://localhost:8080?token=XXX')
ws.onopen = function () {
console.log('socket 连接成功')
ws.send('客户端给服务器发送消息')
}
// 监听服务器发送消息
ws.onmessage = function (messageInfo) {
console.log(messageInfo.data)
}
ws.onerror = function () {
console.log('socket 连接失败')
}
// server.js
// 创建 socket 服务器
const WebSocket = require('ws')
const WebSocketServer = WebSocket.WebSocketServer
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', function connection(ws) {
// 监听客户端传过来的消息
ws.on('message', function message(data, isBinary) {
console.log('received: %s', data)
// 一个客户端 WebSocket 广播到所有其他连接的 WebSocket 客户端,不包括它自己
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary })
}
})
})
// 给客户端发送消息
ws.send('给客户端发送消息')
})
1)客户端
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<input type="text" id="input" />
<button id="btn">发送button>
<script src="/socket.io/socket.io.js">script>
<script>
const socket = io()
btn.onclick = function() {
const input = document.getElementById('input')
const value = input.value
// 给服务端发送消息
socket.emit('chat msg', value)
input.value = ''
}
// 监听服务端发送的消息
socket.on('message', function(msg) {
console.log(msg)
})
script>
body>
html>
2)服务端
const express = require('express')
const app = express()
const server = require('http').Server(app)
const io = require('socket.io')(server)
app.get('/', function (req, res) {
res.sendFile(__dirname + '/public/index.html')
})
// 监听连接
io.sockets.on('connection', function (socket) {
// 获取客户端的消息
socket.on('chat msg', function (msg) {
console.log('msg from client: ' + msg)
// 发送消息给客户端
socket.send('server says: ' + msg)
})
})
server.listen(3000, function () {
console.log('server is running on: 3000')
})