Alex

有的故事值得一直说下去.
Home » Latest Posts

本文目录

一、引言

二、camelot-py 介绍

三、安装 camelot-py

四、camelot-py 使用方法

五、camelot-py 的其他实用参数

六、结束语

七、相关推荐

本文共7015个字,阅读大约需要18分钟,欢迎指正!

一、引言 社科同胞们一定有过收集/整理数据的经历吧,有时候一些原始数据被存放在大量的 PDF 文件中,例如上市公司公告公报中的各种指标信息,但如何快速地从大量的 PDF 中提取出那些表格却是一个难题。在过往的文章中,我们曾向大家分享过使用 Python 的 pdfplumber 库从 PDF 中读取表格的方法(>>>点击查看“一文读懂如何用python读取并处理PDF中的表格”),但经过长期使用,笔者注意到这个库在默认情况下解析时,对表格的要求非常之高。只有当表格的全部框线都存在时才能发挥作用,如果你要读取的表格框线不全,那么读取时极易丢失部分行或列。后来笔者找到了一个在表格框线不全时也能有不错解析效果的工具库,特此与大家分享使用方法和代码。

二、camelot-py 介绍 一个基于 Ghostscript 的库,可以从 PDF 文件中提取表格数据,它使用了一种名为 Lattice 的算法,基于文本的近似排列来解析表,由此实现无框线(或框线不全)表格的解析,解析结果可以直接转为 DataFrame,进而存储为 Excel 表。

三、安装 camelot-py camelot库的安装命令如下:

pip install camelot-py # 常规安装方式 pip install camelot-py[cv] # 常规安装后如果调用报错,卸载后改用此命令, # 表示不仅安装 camelot自身,还会安装其他依赖库 调用时发现camelot依赖PyPDF2库的特定版本,笔者的PyPDF2版本为2.2.2,可以正常运行。

四、camelot-py 使用方法 笔者找到一个仅带有少量框线表格的某上市公司年度报告的 PDF 文件,表格位于第 91 页,如下图:

图片 图片来源:《深圳中航地产股份有限公司二○○七年年度报告》-成本法核算的其他股权投资 下面是使用 camelot 读取该表格的 Python 代码:

可以不导入 pandas,因为导入该库时会自动导入 pandas

import camelot.io as camelot

解析表格

result = camelot.read_pdf( filepath="001914_2007-12-31_2007.pdf", # 94 pages='94',
flavor='stream',
edge_tol=200,
)

解析结果中可能包含多个表格,下面把解析到的第一个表格转为 DataFrame

如果解析结果中不含表格,那么将会报错

df = result[0].df df 解析结果如下图:

图片 从解析结果来看,效果十分不错,唯一的问题在于表头的解析,这就需要对解析结果进行二次清洗,但总的来说效果已经很喜人了。

五、camelot-py 的其他实用参数 让人欣喜的是,camelot并不是一个解析结果只能“看脸”的工具库,它还提供了很多可以干预或优化解析结果的参数,笔者将其中几个必要和实用的参数罗列在下表。 参数名称

取值

描述

filepath

字符串

pdf 文件路径。

pages

字符串,如"91"、"1,2,3"、"91-end"、"all"

从 1 开始算,必须是字符串,可以一次性解析多页,例如:'1,2,3'、'91-end'(表示从91页到最后一页)、'all'(全部页)。

flavor

'lattice'或'stream';默认值为 lattice

针对不同类型的PDF表格指定解析方式,可选参数有'lattice'(格子解析)和'stream'(流解析),前者适用于解析带有完整框线的表格,后者常用于解析框线不全的表格。

edge_tol

数字,默认值为 100

指定表格边缘容差(边缘容忍度)。它是一个浮点数,用于控制识别表格边缘的容差范围。默认值为 100,如果表格的某两行之间间隙稍大,导致表格解析被解析为多个表格,那么可以释放增加该参数的值,避免读取的表格不完整;或者减少参数值,这样当多个表之间的间隙不是特别大时也可以将其分开。

split_text

True 或 False,默认值为 True

当单元格中有分行的文本时,是否应该将它们分为多个单元格。

strip_text

字符串,默认值为 空字符 ''

去除单元格中的指定字符,默认值为'',即不清洗,如果需要取出多种不需要的字符,那么直接将多个字符组合成一个字符串传入即可。

camelot库还有其他有用的参数,如果大家感兴趣,可以去查看源代码,笔者将源码中的参数介绍附在下方:

""" Read PDF and return extracted tables.

Note: kwargs annotated with ^ can only be used with flavor='stream'
and kwargs annotated with * can only be used with flavor='lattice'.

Parameters
----------
filepath : str
    Filepath or URL of the PDF file.
pages : str, optional (default: '1')
    Comma-separated page numbers.
    Example: '1,3,4' or '1,4-end' or 'all'.
password : str, optional (default: None)
    Password for decryption.
flavor : str (default: 'lattice')
    The parsing method to use ('lattice' or 'stream').
    Lattice is used by default.
suppress_stdout : bool, optional (default: True)
    Print all logs and warnings.
layout_kwargs : dict, optional (default: {})
    A dict of `pdfminer.layout.LAParams <https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py#L33>`_ kwargs.
table_areas : list, optional (default: None)
    List of table area strings of the form x1,y1,x2,y2
    where (x1, y1) -> left-top and (x2, y2) -> right-bottom
    in PDF coordinate space.
columns^ : list, optional (default: None)
    List of column x-coordinates strings where the coordinates
    are comma-separated.
split_text : bool, optional (default: False)
    Split text that spans across multiple cells.
flag_size : bool, optional (default: False)
    Flag text based on font size. Useful to detect
    super/subscripts. Adds <s></s> around flagged text.
strip_text : str, optional (default: '')
    Characters that should be stripped from a string before
    assigning it to a cell.
row_tol^ : int, optional (default: 2)
    Tolerance parameter used to combine text vertically,
    to generate rows.
column_tol^ : int, optional (default: 0)
    Tolerance parameter used to combine text horizontally,
    to generate columns.
process_background* : bool, optional (default: False)
    Process background lines.
line_scale* : int, optional (default: 15)
    Line size scaling factor. The larger the value the smaller
    the detected lines. Making it very large will lead to text
    being detected as lines.
copy_text* : list, optional (default: None)
    {'h', 'v'}
    Direction in which text in a spanning cell will be copied
    over.
shift_text* : list, optional (default: ['l', 't'])
    {'l', 'r', 't', 'b'}
    Direction in which text in a spanning cell will flow.
line_tol* : int, optional (default: 2)
    Tolerance parameter used to merge close vertical and horizontal
    lines.
joint_tol* : int, optional (default: 2)
    Tolerance parameter used to decide whether the detected lines
    and points lie close to each other.
threshold_blocksize* : int, optional (default: 15)
    Size of a pixel neighborhood that is used to calculate a
    threshold value for the pixel: 3, 5, 7, and so on.

    For more information, refer `OpenCV's adaptiveThreshold <https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#adaptivethreshold>`_.
threshold_constant* : int, optional (default: -2)
    Constant subtracted from the mean or weighted mean.
    Normally, it is positive but may be zero or negative as well.

    For more information, refer `OpenCV's adaptiveThreshold <https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#adaptivethreshold>`_.
iterations* : int, optional (default: 0)
    Number of times for erosion/dilation is applied.

    For more information, refer `OpenCV's dilate <https://docs.opencv.org/2.4/modules/imgproc/doc/filtering.html#dilate>`_.
resolution* : int, optional (default: 300)
    Resolution used for PDF to PNG conversion.

Returns
-------
tables : camelot.core.TableList

""" 六、结束语 图片

? 提取 PDF 中的表格是研究工作中的一项基础技术工作,传统的表格解析方法难以解决表格框线不全的问题,所以能解析的表格十分有限,而类似于 camelot 这种基于视觉的表格解析方式则强大很多。不过使用 camelot 时也会遇到一些奇怪的问题,例如目录等一些非表格文本可能也会被识别为表格,所以在应用中还需要根据实际情况选择最合适的工具库。

WordPress 提供了强大的 REST API,用于在应用程序之间进行通信。它允许开发者通过标准的 HTTP 请求(如 GET、POST、PUT、DELETE 等)与 WordPress 网站进行交互,获取数据、创建内容或更新数据。以下是 WordPress REST API 的主要端点(Endpoints)和它们的使用方法。

  1. 基础信息 API 基础 URL:https://example.com/wp-json/wp/v2/ 将 example.com 替换为你自己 WordPress 站点的域名。

  2. 通用 REST API 端点格式 GET 请求:从服务器获取数据(如文章、用户、分类等)。 POST 请求:向服务器提交数据,通常用于创建新资源(如创建新文章或用户)。 PUT 请求:更新现有资源。 DELETE 请求:删除资源。

  3. 主要 API 端点

  4. Posts(文章) 获取所有文章: GET /wp-json/wp/v2/posts

获取特定文章: GET /wp-json/wp/v2/posts/{id}

创建文章(需要身份验证): POST /wp-json/wp/v2/posts 参数:title(标题)、content(内容)、status(状态:publish、draft 等)

更新文章: PUT /wp-json/wp/v2/posts/{id}

删除文章: DELETE /wp-json/wp/v2/posts/{id}

  1. Pages(页面) 获取所有页面: GET /wp-json/wp/v2/pages

获取特定页面: GET /wp-json/wp/v2/pages/{id}

创建页面: POST /wp-json/wp/v2/pages

更新页面: PUT /wp-json/wp/v2/pages/{id}

删除页面: DELETE /wp-json/wp/v2/pages/{id}

  1. Categories(分类) 获取所有分类: GET /wp-json/wp/v2/categories

获取特定分类: GET /wp-json/wp/v2/categories/{id}

创建分类: POST /wp-json/wp/v2/categories

更新分类: PUT /wp-json/wp/v2/categories/{id}

删除分类: DELETE /wp-json/wp/v2/categories/{id}

  1. Tags(标签) 获取所有标签: GET /wp-json/wp/v2/tags

获取特定标签: GET /wp-json/wp/v2/tags/{id}

创建标签: POST /wp-json/wp/v2/tags

更新标签: PUT /wp-json/wp/v2/tags/{id}

删除标签: DELETE /wp-json/wp/v2/tags/{id}

  1. Comments(评论) 获取所有评论: GET /wp-json/wp/v2/comments

获取特定评论: GET /wp-json/wp/v2/comments/{id}

创建评论: POST /wp-json/wp/v2/comments

更新评论: PUT /wp-json/wp/v2/comments/{id}

删除评论: DELETE /wp-json/wp/v2/comments/{id}

  1. Users(用户) 获取所有用户: GET /wp-json/wp/v2/users

获取特定用户: GET /wp-json/wp/v2/users/{id}

创建用户: POST /wp-json/wp/v2/users

更新用户: PUT /wp-json/wp/v2/users/{id}

删除用户: DELETE /wp-json/wp/v2/users/{id}

  1. Media(媒体文件) 获取所有媒体文件: GET /wp-json/wp/v2/media

获取特定媒体文件: GET /wp-json/wp/v2/media/{id}

上传媒体文件: POST /wp-json/wp/v2/media 注意:使用 multipart/form-data 格式上传文件。

更新媒体文件: PUT /wp-json/wp/v2/media/{id}

删除媒体文件: DELETE /wp-json/wp/v2/media/{id}

  1. Menus(菜单) WordPress REST API 默认不支持菜单的操作,但可以通过自定义端点或安装插件来支持。

获取菜单(需要插件): GET /wp-json/wp/v2/menus/{id}

  1. 身份验证 REST API 的一些操作(如创建、更新、删除资源)需要进行用户身份验证。常用的验证方式有:

Cookie 验证:适用于 WordPress 登录用户。 应用密码:用户创建应用密码后,应用可以使用用户名和应用密码进行验证。 JWT(JSON Web Token)验证:通过插件(如 JWT Authentication for WP REST API)可以启用基于 JWT 的身份验证。 5. API 查询参数 WordPress REST API 提供了许多可选的查询参数,帮助开发者过滤和排序请求的数据。

常用参数: ?per_page={number}:每页返回的项目数(默认 10,最大 100)。 ?page={number}:分页。 ?search={string}:按关键词搜索内容。 ?orderby={field}:排序字段,如 date、title。 ?order={asc|desc}:排序顺序(升序或降序)。 ?status={status}:筛选特定状态(如 publish、draft)。 6. 自定义端点 开发者可以创建自定义的 REST API 端点来满足特定需求。使用 WordPress 提供的 register_rest_route() 函数可以注册新的 API 端点:

add_action('rest_api_init', function () { register_rest_route('myplugin/v1', '/custom-route/', array( 'methods' => 'GET', 'callback' => 'my_custom_function', )); });

function my_custom_function() { return new WP_REST_Response('Hello, this is a custom API response.', 200); } 7. API 文档与开发工具 官方文档:WordPress REST API 的官方文档提供了全面的使用指南:WordPress REST API 文档 Postman:使用 Postman 等工具可以方便地调试 REST API 请求,测试参数、头部和响应。 通过 WordPress REST API,开发者可以非常灵活地操作 WordPress 网站的数据,构建前后端分离的应用或与其他系统进行集成。

0x01 概述 (1)简介 • Tailwind CSS 官网:https://www.tailwindcss.cn/ • Tailwind CSS 是一个 CSS 框架,使用初级“工具”类创建布局 • 如 Bootstrap 等传统 CSS 框架,其使用的类通常与组件直接相关;然而,Tailwind 则采用了不同的方法,它将类作为工具集合,让用户能够自由组合这些工具来构建个性化的自定义组件 • 工具类是简单的 HTML 类,其作用域通常为单个和特定 CSS 属性,具有以下优势 • 根据目的命名 • 易于理解和记忆 • 作用浅显易懂 • 不存在命名不一致 • 支持快速布局创建和测试 • Tailwind 有一个条件类,用于为断点、响应状态等命名 (2)基本环境配置 • 代码文本编辑工具:VSCode 或其他 • 推荐插件: • 标签重命名:Auto Rename Tag • 实时加载:Live Server • PostCSS 语法支持:PostCSS Language Support • 代码格式化:Prettier • Tailwind 官方补全:Tailwind CSS IntelliSense • 包管理器:Node.js 或其他 • (可选)版本控制:Git (3)创建项目

  1. 在任意位置新建一个 index.html
  2. 使用 CDN 引入 Tailwind:https://cdn.tailwindcss.com/ 1

2

3 4 5 6 7 <title>Document</title> 8 <script src="https://cdn.tailwindcss.com/"></script> 9 10 11 0x02 基本功能 (1)使用颜色 • 默认包含的颜色: black #000000 white #FFFFFF red #EF4444 green #22C55E blue #3B82F6 orange #F97316 yellow #EAB308 purple #A855F7 lime #84CC16 emerald #10B981 teal #14B8A6 cyan #06B6D4 indigo #6366F1 violet #8B5CF6 fuchsia #D946EF pink #EC4899 rose #F43F5E sky #0EA5E9 gray #6B7280 slate #64748B zinc #71717A neutral #737373 stone #78716C amber #F59E0B • 文本颜色:text-[color]-[shade] • color:颜色名称 • shade:色度,取值范围为 100~900,不可对黑色或白色使用 1

Color Black

2

Color White

3

Color Red 500

4

Color Blue 500

5

Color Gray 500

6

Color Green 500

• 背景颜色:bg-[color]-[shade] 1

BgColor White

2

BgColor Red 500

3

BgColor Black

• 下划线颜色:underline decoration-[color]-[shade] • underline:添加下划线 1

UlColor Default

2

UlColor Red 500

3

UlColor Green 500

• 边框颜色:border border-[color]-[shade] • border:添加边框 1 2 3 • 间隔颜色:divide-[direct] divide-[color]-[shade] • divide-[direct]:添加分隔,direct 表示分隔方向,取值 x-横向、y-纵向 1
2
Item 1
3
Item 2
4
Item 3
5
• 轮廓线颜色:outline outline-[color]-[shade] • outline:添加轮廓线 1 2 3 • 盒子阴影颜色:shadow-[container] shadow-[color]-[shade(/opacity)] • shadow-[container]:添加阴影,container 表示阴影最大宽度,取值如下 container width sm 640px md 768px lg 1024px xl 1280px 2xl 1536px • (/opacity):透明度,默认 100,取值 0~100 1
SdColor Red 500
2
SdColor Red 500
3
SdColor Red 500
• 强调颜色:accent-[color]-[shade] 1 2 3 • 自定义颜色:-[(#xxxxx|rgb(r,g,b)|name)] • 十六进制、RGB、名称 1

Color #4682B4

2

Color RGB(46,130,180)

3

Color Steelblue

(2)容器与间距 • 容器 1
2
3

Title

4

Tailwind CSS is the only framework that I've 5 seen scale on large teams. It’s easy to customize, 6 adapts to any design, and the build size is tiny.

7
8
• container:标记为容器 • mx-auto:x 轴方向(横向)上,外边距(margin)自动 • 外边距:m?-[number] • m?:m-Margin、mt-MarginTop、ml-MarginLeft、mr-MarginRight、mb-MarginBottom • number=number * 0.25rem=number * 4px • 如:m-2 意思为 margin: 0.5rem 1
Margin 2
2
Margin Left 2
3
Margin Right 2
4
Margin Top 2
5
Margin Bottom 2
6
Margin 20px
• 内边距:p?-[number] • p?:p-Padding、pt-PaddingTop、pl-PaddingLeft、pr-PaddingRight、pb-PaddingBottom • number=number * 0.25rem=number * 4px 1
Padding 2
2
Padding Left 2
3
Padding Right 2
4
Padding Top 2
5
Padding Bottom 2
6
Padding 20px
• 间距:(-)space-[direct]-[number] • (-):负间距 • direct:间隔方向,取值 x-横向、y-纵向 • number=number * 0.25rem=number * 4px 1
2
Item 1
3
Item 2
4
Item 3
5
6
7
Item 1
8
Item 2
9
Item 3
10
• flex:采用 Flex 布局 • flex-col:Flex 布局纵向 (3)版面设计 • 字体:font-[family] 1
Font Sans
2
Font Serif
3
Font Mono
• 自定义字体族配置 1 2 > 3 <script> 4 tailwind.config = { 5 theme: { 6 fontFamily: { 7 sans: ["Inter", "sans-serif"], 8 serif: ["Inter", "serif"], 9 mono: ["Inter", "monospace"] 10 } 11 } 12 } 13 </script> 14 • 字号:text-[size] 1
Text Extra Small
2
Text Small
3
Text Base
4
Text Large
5
Text Extra Large
6
Text 2 Extra Large
7
Text 3 Extra Large
• 字重:font-[weight] 1
Font Light
2
Font Normal
3
Font Medium
4
Font Semibold
5
Font Bold
• 字距:tracking-[space] 1
Tracking Tight
2
Tracking Normal
3
Tracking Wide
• 文本对齐:text-[direct] 1
Text Left
2
Text Center
3
Text Right
• 下划线重:decoration-[weight] 1
Decoration 2
2
Decoration 4
3
Decoration 8
• 下划线风格:decoration-[type] 1
Decoration Double
2
Decoration Dotted
3
Decoration Dashed
4
Decoration Wavy
• 装饰偏移量:underline-offset-[number] 1
Offset 2
2
Offset 4
3
Offset 8
• 文本变换: 1

Normal Case

2

uppercase

3

LOWERCASE

4

capitalize test

(4)宽度与高度 • 宽度:w-[number] • number 取值 0~48 1
Width 1.5rem
2
Width 3rem
3
Width 6rem
• 百分比:w-[number_1]/[number_2] 1
Width 25%
2
Width 33%
3
Width 50%
• 视点宽度 1
Width 100vw
• 100% 容器 1
Width 100%
• 自定义宽度 1
Width 300px
• 最大宽度:max-w-[size] 1
2 Tailwind CSS is the only framework that I've 3 seen scale on large teams. It’s easy to customize, 4 adapts to any design, and the build size is tiny. 5
• 高度(大部分与宽度方法相同):h-[number] • number 取值 0~96 1
2
Height 24
3
Height 48
4
Height 72
5
Height 96
6
• 全屏高度 1
Height 100vh
(5)布局与定位 • 定位:相对定位与绝对定位 1
2

Parent

3
4

Child

5
6
• 左上角 1
2
3
• 右上角 1
2
3
• 左下角 1
2
3
• 右下角 1
2
3
• 顶边 1
2
3
• 左边 1
2
3
• 右边 1
2
3
• 底边 1
2
3
• 显示方式 1
2 Tailwind CSS is the only framework that 3 (Inline) 4 I've seen scale on large teams. 5 (Inline-block) 6 It’s easy to customize, adapts to any design, 7 (Block) 8 and the build size is tiny. 9 10
• 层叠等级(Z 轴索引):z-[number/auto] 1
2
1
3
2
4
3
5
4
6
5
7
• 浮动:float-[left/right/none] 1
2
1
3
2
4
(6)背景与阴影 • 背景 1
• 大小 bg-auto bg-cover bg-contain • 重复 bg-repeat bg-no-repeat bg-repeat-x bg-repeat-y bg-repeat-round bg-repeat-space • 定位 bg-center bg-top bg-bottom bg-left bg-left-top bg-left-bottom bg-right bg-right-top bg-right-bottom • attachment bg-fixed bg-local bg-scroll • 渐变:bg-gradient-to-[direct] from-[color]-[shade] to-[color]-[shade] 1
• 阴影 1
2 Consectetur velit laboris tempor laboris qui consequat eu minim ipsum nulla culpa aliquip ad. 3
Tailwind Background Class CSS Code shadow-sm box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); shadow box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); shadow-md box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); shadow-lg box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); shadow-xl box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1) 0 8px 10px -6px rgb(0 0 0 / 0.1); shadow-2xl box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); shadow-inner box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); shadow-none box-shadow: 0 0 #0000; • 混合 1
2
3 Id ex deserunt velit excepteur deserunt tempor eu aliquip ipsum labore laboris. 4
5
6 Adipisicing voluptate magna aute sunt consequat irure sint. 7
8
(7)边框 • 宽度:border(-[direct])-[width] • (-[direct]):取值为 t、l、r、b、x 或 y,分别对应上、左、右、下、左右或上下 • width:取值为 0、2、4 或 8,分别对应 0px、2px、4px、8px 1
2 Amet commodo nisi quis irure velit Lorem enim anim commodo sunt aliquip officia quis. 3
4
5 Amet commodo nisi quis irure velit Lorem enim anim commodo sunt aliquip officia quis. 6
7
8 Amet commodo nisi quis irure velit Lorem enim anim commodo sunt aliquip officia quis. 9
• 颜色 详见本章第(1)小节“边框颜色”部分 • 圆角:rounded(-[direct])(-[size]) • (-[direct]):取值为 t、l、r、b、x 或 y,分别对应上、左、右、下、左右或上下 • (-[size]):取值为 none、sm、【略】、md、lg、xl、2xl、3xl、full 1
2 Dolore deserunt sunt qui ut quis sunt anim do nostrud minim fugiat minim. 3
• 轮廓线:outline(-[width]) • (-[width]):取值为 0、1、2、4、8 1 (8)过滤器 • 模糊:blur(-[size]) • (-[size]):取值为 none、sm、【略】、md、lg、xl、2xl、3xl 1
2 Do elit adipisicing cupidatat dolor excepteur nulla in incididunt. 3
• 亮度:brightness-[number] • number:filter: brightness(number*0.1) 1
Brightness 0
2
Brightness 100
3
Brightness 200
• 对比度:contrast-[number] • number:filter: contrast(number*0.1) 1
Contrast 0
2
Contrast 50
3
Contrast 200
• 灰度 1
Grayscale
• 反色 1
Invert
• 褐化 1
Sepia
• Hue rotate:hue-rotateo-[number] • number:取值为 0、15、30、60、90、180 1
Hue Rotate 0
2
Hue Rotate 90
3
Hue Rotate 180
0x03 进阶应用 (1)交互 • 悬停:hover: 1 • 聚焦:focus: 1 • 触发:active: 1 • 设置内部元素样式 1
2

Title

3

Laboris tempor ex nisi deserunt labore anim et do in officia sint laborum.

4
• 伪类 1
    2
  • Item 1
  • 3
  • Item 2
  • 4
  • Item 3
  • 5
  • Item 4
  • 6
  • Item 5
  • 7
• 外观 • 一般用于重置浏览器或操作系统默认的样式外观 1 5 • 游标:cursor-[type] • type 常见取值为 auto default pointer wait text move help not-allowed none context-menu progress cell crosshair vertical-text ... 1
• 选中 1
Select None
2
Select Text
3
Select All
4
Select Auto
• 平滑滚动示例 1 2 3 4 5 <script src="https://cdn.tailwindcss.com/"></script> 6 7 8 Go to Bottom 9
10
Bottom
11 12 (2)断点类与媒体查询 1. 窗口宽度实时监听 1 2

3 <script> 4 function showBrowserWidth() { 5 const width = window.innerWidth 6 document.querySelector('h1').innerText = `Width: ${width}` 7 } 8 window.onload = showBrowserWidth 9 window.onresize = showBrowserWidth 10 </script> 11 2. 修改 h1 标签样式,设置默认背景颜色 1 2

3 <script> 4 function showBrowserWidth() { 5 const width = window.innerWidth 6 document.querySelector('h1').innerText = `Width: ${width}` 7 } 8 window.onload = showBrowserWidth 9 window.onresize = showBrowserWidth 10 </script> 11 3. 在 body 标签中,根据窗口宽度设置背景颜色变化 1 2 3 size min-width sm 640px md 768px lg 1024px xl 1280px 2xl 1536px (3)分栏 • 基本列:columns-[number/type] • number:取值 1~12 • type:取值 auto、3xs、2xs、xs、sm、md、lg、xl、2xl、3xl、4xl、5xl、6xl、7xl 1
2
Proident ipsum consequat dolor deserunt.
3
Proident ipsum consequat dolor deserunt.
4
Proident ipsum consequat dolor deserunt.
5
Proident ipsum consequat dolor deserunt.
6
• 纵横比:aspect-[type] 1
2
Proident ipsum consequat dolor deserunt.
3
Proident ipsum consequat dolor deserunt.
4
(4)Flexbox • Flex 与对齐 1. 创建一个 Flexbox 1
2
1
3
2
4
3
5
4
6
2. 设置每项的对齐方向:items-[type] 1
2 3
3. 设置每项的对齐方式:justify-[type] 1
2 3
4. 设置换行,使某一项超出窗口:flex-wrap 1
2
1
3
2
4
3
5
4
6
• 列、间隙、顺序 1. 创建一个 Flexbox,设置为纵向排列 1
2
1
3
2
4
3
5
4
6
2. 设置间隙:gap-[number](number * 0.25rem) 1
2 3
3. 调整顺序:order-[index] 1
2
1
3
2
4
3
5
4
6
• 放大与缩小 1
2
1
3
2
4
3
5
4
6
5
7
(5)Grid • 模板列:grid-cols-[number/none] • number:取值为 1~12 • 该语句等同于:grid-template-columns: repeat(number, minmax(0, 1fr)); 1
2
Item 1
3
Item 2
4
Item 3
5
Item 4
6
Item 5
7
Item 6
8
Item 7
9
Item 8
10
Item 9
11
• 模板行:grid-rows-[number/none] • number:取值为 1~6 • 该语句等同于:grid-template-rows: repeat(number, minmax(0, 1fr)); 1
2 3
• 跨行/列:[row/col]-span-[number] • row-span-[number]:跨 number 行 • col-span-[number]:跨 number 列 1
2
Item 1
3
Item 2
4 5
(6)过渡与变换 初始按钮: 1 • 过渡 • transition-[type]:过渡类型,type 取值为 none、all、colors、opacity、shadow、transform • duration-[ms]:过渡时长,ms 取值为 75、100、150、200、300、500、700、1000 • ease-[type]:过渡效果,type 取值为 linear、in、out、in-out • delay-[ms]:延时时长,ms 取值为 75、100、150、200、300、500、700、1000 1 • 变换 • scale(-[axis])-[magnification]:缩放,axis 表示参照轴,取值为 x 或 y;magnification 表示缩放倍率,为 number% • rotate-[degree]:旋转,degree 表示旋转角度 • translate-[axis]-[distance]:位移,axis 表示参照轴,取值为 x 或 y;distance 表示位移距离,整数为 number * 0.25rem,分数为占比,full 为 100%,px 为 1px • skew-[direct]-[degree]:倾斜,axis 表示参照轴,取值为 x 或 y;degree 表示倾斜角度 1 (7)动画类与关键帧 • animate-[property]:property 取值如下: • none:animation: none; • spin:旋转 1 animate: spin 1s linear infinite; 2 @keyframes spin { 3 from { 4 transform: rotate(0deg); 5 } 6 to { 7 transform: rotate(360deg); 8 } 9 } • ping:扩散 1 animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; 2 @keyframes ping { 3 75%, 100% { 4 transform: scale(2); 5 opacity: 0; 6 } 7 } • pulse:闪烁 1 animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 2 @keyframes pulse { 3 0%, 100% { 4 opacity: 1; 5 } 6 50% { 7 opacity: .5; 8 } 9 } • bounce:弹跳 1 animation: bounce 1s infinite; 2 @keyframes bounce { 3 0%, 100% { 4 transform: translateY(-25%); 5 animation-timing-function: cubic-bezier(0.8, 0, 1, 1); 6 } 7 50% { 8 transform: translateY(0); 9 animation-timing-function: cubic-bezier(0, 0, 0.2, 1); 10 } 11 } 1
2
3
• 自定义动画与关键帧 1 2 3 4 5 <script> 6 tailwind.config = { 7 theme: { 8 extend: { 9 animation: { 10 'slow-spin': 'spin 3s linear infinite', 11 'custom': 'custom 1s linear infinite' 12 }, 13 keyframes: { 14 'custom': { 15 '0%': { 16 transform: 'rotate(0deg)' 17 }, 18 '90%, 100%': { 19 transform: 'rotate(360deg)' 20 } 21 } 22 } 23 } 24 } 25 } 26 </script> 27 28 29
30
31
32 33 (8)配置与客制化 • 在 head 标签中的 script 标签中写入配置 • 配置格式: 1 <script> 2 tailwind.config = { 3 theme: { 4 extend: { 5 'property-1': { 6 'key-1-1': 'value-1-1', 7 }, 8 'property-2': { 9 'key-2-1': 'value-2-1', 10 }, 11 // ... 12 } 13 } 14 } 15 </script> (9)夜间模式 • 配置夜间模式样式:dark: 1
• 是否使用夜间模式,默认根据操作系统 • 使用 Javascript 控制夜间模式 1 2 3 4 5 <script> 6 tailwind.config = { 7 darkMode: "class", 8 }; 9 </script> 10 <style> 11 .toggle-checkbox:checked { 12 right: 0; 13 border-color: #68d391; 14 } 15 .toggle-checkbox:checked + .toggle-label { 16 background: #68d391; 17 } 18 </style> 19 20 21
24

25 Title 26

27

28 Irure dolor ut excepteur ea cupidatat dolor dolore consectetur sit 29 voluptate qui et deserunt. 30

31
32
35 41 45
46 <script> 47 document.getElementById("toggle").addEventListener("change", function () { 48 if (this.checked) { 49 document.documentElement.classList.add("dark"); 50 } else { 51 document.documentElement.classList.remove("dark"); 52 } 53 }); 54 </script> 55 56 (10)分组 • 子选择:group[/name],如: 1 2
3
4
5 • 兄弟选择:peer[/name],如: 1 2
3
4 • 注意,兄弟选择仅支持对被命名的元素后面的元素选择 0x04 更好的开发环境 (1)基于 Tailwind CLI 构建环境 之前使用 CDN 的方式使用 Tailwind 并非生产的最佳选择,Tailwind 还支持使用 Tailwind CLI 等方式 1. 创建一个目录作为 Tailwind 项目的根目录 2. 在该目录下,使用命令 npm init -y 创建一个 package.json 作为包管理 3. 使用命令 npm install -D tailwindcss 引入 Tailwind 4. 使用命令 npx tailwindcss init 创建用于初始化 Tailwind 的配置文件—— tailwind.config.js 5. 修改 tailwind.config.js,允许 Tailwind 控制当前目录下所有后缀为 html 的文件 1 /** @type {import('tailwindcss').Config} */ 2 module.exports = { 3 content: ['./*.html'], 4 theme: { 5 extend: {}, 6 }, 7 plugins: [], 8 } 6. 创建 input.css 作为主 CSS 文件 1 @tailwind base; 2 @tailwind components; 3 @tailwind utilities; 7. 修改 package.json,配置指令实现开启 Tailwind CLI 构建流程 1 { 2 // ... 3 "scripts": { 4 "build": "tailwindcss -i ./input.css -o ./css/style.css", 5 "watch": "tailwindcss -i ./input.css -o ./css/style.css --watch", 6 "test": "echo \"Error: no test specified\" && exit 1" 7 }, 8 // ... 9 } 8. 使用命令 npm run build 构建 CSS 文件及其目录 9. 创建 index.html,其中引入生成的 style.css,并添加一个标签使用 Tailwind 1 2 3 4 5 6 7 8 <title>Document</title> 9 10 11

Tailwind CSS

12 13 10. 使用命令 npm run watch 实时使用 Tailwind 渲染页面 (2)指令与函数 a. 指令 指令是 Tailwind 特定于可以在CSS中使用的规则,这些规则为 Tailwind 项目提供特殊的功能 • @tailwind:用于插入 Tailwind 的基本(base)、组件(components)以及工具类(utilities)等 • @apply:用于获取工具类中的样式 1 h1 { 2 @apply text-xl; 3 } • @layer:用于创建自定义样式 1 @layer base { 2 h1 { 3 font-size: 2rem; 4 } 5 } 6 @layer components { 7 btn-blue { 8 @apply bg-blue-500 px-4 py-2 rounded-xl font-bold hover:bg-blue-700; 9 } 10 } • @config:用于指定 Tailwind 在编译该 CSS 文件时应使用的配置文件 1 @config "./tailwind.input.config.js"; b. 函数 函数用于访问 Tailwind 中的特定值,构建后会使用静态值替换 • theme():可以使用点表示法访问 Tailwind 配置值 1. 修改 tailwind.config.js 1 /** @type {import('tailwindcss').Config} */ 2 module.exports = { 3 content: ['./*.html'], 4 theme: { 5 extend: { 6 spacing: { 7 128: '32rem' 8 } 9 }, 10 }, 11 plugins: [], 12 } 2. 修改 input.css,引入 spacing-128 1 .content { 2 height: theme('spacing.128'); 3 } • screen():可以创建按名称引用断点的媒体查询 1 @media screen(xl) { 2 body { 3 @apply bg-black text-white; 4 } 5 } (3)使用 Webpack 与 PostCSS 环境 • Webpack 是一个模块打包工具,可以将带有依赖的模块打包成静态资源,官网链接 • PostCSS 是一个用 JavaScript 工具和插件转换 CSS 代码的工具,官网链接 1. 创建一个目录作为 Tailwind 项目的根目录 2. 在该目录下,使用命令 npm init -y 创建一个 package.json 作为包管理 3. 使用命令 npm install -D webpack webpack-cli webpack-dev-server style-loader css-loader postcss postcss-loader postcss-preset-env 安装 Webpack 与 PostCSS 以及相关加载器 4. 在根目录创建 src 目录,其中新建 index.js,作为 Webpack 的入口文件 5. 在根目录创建 webpack.config.js 文件,用于配置 Webpack 1 const path = require('path') 2 3 module.exports = { 4 mode: 'development', 5 entry: './src/index.js', 6 output: { 7 path: path.resolve(__dirname, 'dist'), 8 filename: 'bundle.js', 9 }, 10 } 6. 修改 package.json,配置指令实现开启 Webpack 构建流程 1 { 2 // ... 3 "scripts": { 4 "build": "webpack" 5 }, 6 // ... 7 } 7. 使用命令 npm run build 构建主 Javascript 文件及其目录 dist 8. 在 dist 目录下,创建 index.html,其中引入生成的 bundle.js 1 2 3 4 5 6 7 <title>Document</title> 8 <script src="bundle.js"></script> 9 10 11

Tailwind CSS

12 13 9. 修改 webpack.config.js,设置 Webpack 开发服务器,用于实时使用渲染页面 1 const path = require('path') 2 3 module.exports = { 4 // ... 5 devServer: { 6 static: { 7 directory: path.resolve(__dirname, 'dist'), 8 }, 9 port: 9000, 10 open: true, 11 hot: true, 12 compress: true, 13 historyApiFallback: true, 14 }, 15 } 10. 修改 package.json,配置指令实现开启 Webpack 构建流程 1 { 2 // ... 3 "scripts": { 4 "build": "webpack", 5 "dev": "webpack serve" 6 }, 7 // ... 8 } 11. 使用命令 npm run dev 开启 Webpack 开发服务器 12. 修改 webpack.config.js,设置样式加载器 1 const path = require('path') 2 3 module.exports = { 4 // ... 5 module: { 6 rules: [ 7 { 8 test: /\.css$/i, 9 include: path.resolve(__dirname, 'src'), 10 use: ['style-loader', 'css-loader', 'postcss-loader'], 11 }, 12 ], 13 }, 14 } 13. 使用命令 npm install -D tailwindcss 引入 Tailwind 14. 使用命令 npx tailwindcss init 创建用于初始化 Tailwind 的配置文件—— tailwind.config.js 15. 修改 tailwind.config.js,允许 Tailwind 控制当前目录下所有后缀为 html 的文件 1 /** @type {import('tailwindcss').Config} */ 2 module.exports = { 3 content: ['./dist/*.{html, js}'], 4 theme: { 5 extend: {}, 6 }, 7 plugins: [], 8 } 16. 在根目录创建 postcss.config.js 文件,用于配置 PostCSS 1 const tailwindcss = reuqire('tailwindcss'); 2 3 module.exports = { 4 plugins: [ 5 'postcss-preset-env', 6 tailwindcss 7 ], 8 }; 17. 在 src 目录创建 style.css 作为主 CSS 文件 1 @tailwind base; 2 @tailwind components; 3 @tailwind utilities; 18. 修改 src/index.js,将 style.css 添加到入口文件 1 import './style.css' -End-

差不多一个星期没更新这个系列了,今天难得有点时间,然后就开始写了点代码,上一章节讲了数据模型的定义和数据发送。这些都是一些准备,但是实际上距离真正实现tcp内网穿透代理还有些距离。

所以今天的章节是快速写一个例子,来测试一下tcp内网穿透代理。然后再规范代码。

快速的demo测试,可以立马看到效果。

我自己对于写代码的理解是:

分为以下几步

1.首先需要规划,设计,考虑要做的事情,比如接口怎么定义,数据结构是怎么样的,代码逻辑,业务逻辑,流程是怎样的,这些都是需要梳理清楚的。

2.那就是根据这些东西先来个快速的编码,先不要管细节,能越快实现就越好。这一步就是不需要过度的按照设计去实现东西。

3.最后就是根据我们快速编码实现的效果,去改进优化。实现的更加优雅,通用。

今天要做的就是第二步,来快速实现一个tcp内网穿透代理。

首先还是我们需要一个http服务器,这个http服务器是我们的内网的服务器,也就是说我们需要在外网访问到这个位于内网的http服务器。假设我们内网的ip是127.0.0.1,分配的局域网ip是192.168.1.10,然后http端口是8080

那么显而易见,我们在同一内网环境是可以访问的,直接使用192.168.1.10:8000即可访问到服务器

但是如果不在同一局域网的机器就不行了,需要借助一台公网ip的服务器来做一个透传代理。

内网服务器准备 这里假设你已经安装python2或者python3,打开我们的mac终端或者windows cmd 在python2下输入python -m SimpleHTTPServer

python3下输入python -m http.server

这样我们可以快速得到一台http服务器,打开浏览器输入127.0.0.1:8000可以发现是一个文件浏览的http服务器

我们不需要很复杂的http服务器,仅仅用来做测试而已,所以这样是足够的了

服务端代码 控制客户端的监听代码 1.这里选择监听在8009端口,这个tcp服务,主要用来接受客户端的连接请求的,然后发送控制指令给到客户端,请求建立隧道连接的。这里只接受一个客户端的连接请求,如果有多余的会close掉

一旦有客户端连接到8009端口,这个tcp连接是一直保持的,为什么呢?

因为服务端需要发送控制指令给客户端,所以tcp连接必须一直保持。

然后服务端会每隔两秒发送hi这个消息给到客户端,客户端可以直接忽略掉,因为这个hi只是类似心跳机制的保证。

var cache *net.TCPConn = nil func makeControl() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8009") //打开一个tcp断点监听 tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } fmt.Println("控制端口已经监听") for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { panic(err) } fmt.Println("新的客户端连接到控制端服务进程:" + tcpConn.RemoteAddr().String()) if cache != nil { fmt.Println("已经存在一个客户端连接!") //直接关闭掉多余的客户端请求 tcpConn.Close() } else { cache = tcpConn } go control(tcpConn) }

func control(conn *net.TCPConn) { go func() { for { //一旦有客户端连接到服务端的话,服务端每隔2秒发送hi消息给到客户端 //如果发送不出去,则认为链路断了,清除cache连接 _, e := conn.Write(([]byte)("hi\n")) if e != nil { cache = nil } time.Sleep(time.Second * 2) } }() } 对外访问的服务端口监听 假设端口是8007,这里的对外访问的服务端口监听,也就是说假设我们服务器ip是10.18.10.1的话,那么访问10.18.10.1的端口8007,就等于请求内网的127.0.0.1:8000 127.0.0.1:8000就是上面的python服务器

和上面的代码看起来很像,但是用处不一样,上面那个主要目的是控制客户端,要求它建立请求

这里的目的主要是提供真正需要tcp代理透传的服务!

func makeAccept() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8007") tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } defer tcpListener.Close() for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(err) continue } fmt.Println("A client connected 8007:" + tcpConn.RemoteAddr().String()) addConnMathAccept(tcpConn) sendMessage("new\n") } } 这里大家思考一下,如果真的有请求来了,也就是访问8007了,我们怎么办呢?显然我们需要把进来的流量发给127.0.0.1:8000,让它去处理就行了。

这么一想好像很简单的样子,但是好像有问题,那就是我的10.18.10.1是公网ip啊,大家都知道,只有非公网可以主动访问公网,非公网主动访问公网的意思就是好像我们日常访问百度一样。公网是不可以直接跟非公网建立tcp连接的。

那么怎么解决呢?

那就是我们需要先记录下这个进来的8007的tcp连接,然后上面不是说到我们有个tcp连接是一直hold住的,那就是8009那个,服务器每隔2秒发送hi给客户端的。

那么我们可以通过这个8009的tcp链路发送一条消息给客户端,告诉客户端赶紧和我建立一个新的tcp请求吧,为了方便描述,我把"告诉客户端赶紧和我建立一个新的tcp请求"这个新的请求标记为8008链路

这时候就可以把8007的tcp流量发送到这个新建立的tcp链路上。然后把这个新建立的tcp链路的请求发送回去,建立一个读写传输链路即可。

注意这里不能使用8009的tcp链路,8009只是我们用来沟通的链路。

理清楚后,开始编码吧

记录进来的8007的tcp连接,使用一个结构体来存储,这个结构体需要记录accept的tcp连接,也就是8007的tcp链路,和请求的时间,以及8008的链路

刚开始记录的时候8008的链路肯定是nil的,所以设置为nil即可

把它添加到map里面。使用unixNano作为临时key

type ConnMatch struct { accept *net.TCPConn //8007 tcp链路 accept acceptAddTime int64 //接受请求的时间 tunnel *net.TCPConn //8008 tcp链路 tunnel } var connListMap = make(map[string]*ConnMatch) var lock = sync.Mutex{} func addConnMathAccept(accept *net.TCPConn) { //加锁防止竞争读写map lock.Lock() defer lock.Unlock() now := time.Now().UnixNano() connListMap[strconv.FormatInt(now, 10)] = &ConnMatch{accept, time.Now().Unix(), nil} } 告诉客户端赶紧和我建立一个新的tcp请求

这里直接用上面那个cache的tcp链路发送消息即可,不需要太复杂,这里简单定义为new\n即可

........ addConnMathAccept(tcpConn) sendMessage("new\n") } } func sendMessage(message string) { fmt.Println("send Message " + message) if cache != nil { _, e := cache.Write([]byte(message)) if e != nil { fmt.Println("消息发送异常") fmt.Println(e.Error()) } } else { fmt.Println("没有客户端连接,无法发送消息") } } 转发的tcp监听服务 这里我们来创建前面提到的8008tcp连接了,这里的8008端口,也就是前面说的发送new这个消息告诉客户端来和这个8008连接吧

func makeForward() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8008") tcpListener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { panic(err) } defer tcpListener.Close() fmt.Println("Server ready to read ...") for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { fmt.Println(err) continue } fmt.Println("A client connected 8008 :" + tcpConn.RemoteAddr().String()) configConnListTunnel(tcpConn) } } 然后把8008链路分配到ConnMatch,这两个tcp链路是配对的

var connListMapUpdate = make(chan int) func configConnListTunnel(tunnel *net.TCPConn) { //加锁解决竞争问题 lock.Lock() used := false for _, connMatch := range connListMap { //找到tunnel为nil的而且accept不为nil的connMatch if connMatch.tunnel == nil && connMatch.accept != nil { //填充tunnel链路 connMatch.tunnel = tunnel used = true //这里要break,是防止这条链路被赋值到多个connMatch! break } } if !used { //如果没有被使用的话,则说明所有的connMatch都已经配对好了,直接关闭多余的8008链路 fmt.Println(len(connListMap)) _ = tunnel.Close() fmt.Println("关闭多余的tunnel") } lock.Unlock() //使用channel机制来告诉另一个方法已经就绪 connListMapUpdate <- UPDATE } tcp 转发,这里读取connListMapUpdate这个chain,说明map有更新,需要建立tcpForward隧道

func tcpForward() { for { select { case <-connListMapUpdate: lock.Lock() for key, connMatch := range connListMap { //如果两个都不为空的话,建立隧道连接 if connMatch.tunnel != nil && connMatch.accept != nil { fmt.Println("建立tcpForward隧道连接") go joinConn2(connMatch.accept, connMatch.tunnel) //从map中删除 delete(connListMap, key) } } lock.Unlock() } } } func joinConn2(conn1 *net.TCPConn, conn2 *net.TCPConn) { f := func(local *net.TCPConn, remote *net.TCPConn) { //defer保证close defer local.Close() defer remote.Close() //使用io.Copy传输两个tcp连接, _, err := io.Copy(local, remote) if err != nil { fmt.Println(err.Error()) return } fmt.Println("join Conn2 end") } go f(conn2, conn1) go f(conn1, conn2) } 最后增加一个超时机制,因为会存在这种情况,就是当用户请求8007端口的时候,迟迟等不到配对的connMatch的tunnel链路啊,因为有可能client端挂掉了,导致server不管怎么发送"new"请求,client都无动于衷。

在浏览器看来表现就是一直转着,但是我们不能这样子。

所以当超时的时候,我们主动断掉connMatch中的accept链路即可,设置为5秒

func releaseConnMatch() { for { lock.Lock() for key, connMatch := range connListMap { //如果在指定时间内没有tunnel的话,则释放该连接 if connMatch.tunnel == nil && connMatch.accept != nil { if time.Now().Unix()-connMatch.acceptAddTime > 5 { fmt.Println("释放超时连接") err := connMatch.accept.Close() if err != nil { fmt.Println("释放连接的时候出错了:" + err.Error()) } delete(connListMap, key) } } } lock.Unlock() time.Sleep(5 * time.Second) } } 最后把所有方法整合起来

func main() { //监听控制端口8009 go makeControl() //监听服务端口8007 go makeAccept() //监听转发端口8008 go makeForward() //定时释放连接 go releaseConnMatch() //执行tcp转发 tcpForward() } 客户端代码 连接到服务器的8009控制端口,随时接受服务器的控制请求,随时待命

func connectControl() { var tcpAddr *net.TCPAddr //这里在一台机测试,所以没有连接到公网,可以修改到公网ip tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8009") conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Println("Client connect error ! " + err.Error()) return } fmt.Println(conn.LocalAddr().String() + " : Client connected!8009") reader := bufio.NewReader(conn) for { s, err := reader.ReadString('\n') if err != nil || err == io.EOF { break } else { //接收到new的指令的时候,新建一个tcp连接 if s == "new\n" { go combine() } if s == "hi" { //忽略掉hi的请求 } }

}

} combine方法的代码,整合local和remote的tcp连接

func combine() { local := connectLocal() remote := connectRemote() if local != nil && remote != nil { joinConn(local, remote) } else { if local != nil { err := local.Close() if err!=nil{ fmt.Println("close local:" + err.Error()) } } if remote != nil { err := remote.Close() if err!=nil{ fmt.Println("close remote:" + err.Error()) }

    }
}

} func joinConn(local *net.TCPConn, remote *net.TCPConn) { f := func(local *net.TCPConn, remote *net.TCPConn) { defer local.Close() defer remote.Close() _, err := io.Copy(local, remote) if err != nil { fmt.Println(err.Error()) return } fmt.Println("end") } go f(local, remote) go f(remote, local) } connectLocal 连接到python的8000端口!

func connectLocal() *net.TCPConn { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8000")

conn, err := net.DialTCP("tcp", nil, tcpAddr)

if err != nil {
    fmt.Println("Client connect error ! " + err.Error())
    return nil
}

fmt.Println(conn.LocalAddr().String() + " : Client connected!8000")
return conn

} connectRemote 连接到服务端的8008端口!

func connectRemote() *net.TCPConn { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:8008")

conn, err := net.DialTCP("tcp", nil, tcpAddr)

if err != nil {
    fmt.Println("Client connect error ! " + err.Error())
    return nil
}
fmt.Println(conn.LocalAddr().String() + " : Client connected!8008")
return conn;

} 全部整合起来就是

func main() { connectControl() } 最后把服务端和客户端运行起来看看效果,访问8007,而不是访问8000

这篇文章主要为大家详细介绍了Golang实现内网穿透的相关知识,包括原理和代码实现,文中的示例代码讲解详细,有需要的小伙伴可以参考一下 脚本之家 / 编程助手:解决程序员“几乎”所有问题! 脚本之家官方知识库 → 点击立即使用

我们经常会遇到一个问题,如何将本机的服务暴露到公网上,让别人也可以访问。我们知道,在家上网的时候我们有一个 IP 地址,但是这个 IP 地址并不是一个公网的 IP 地址,别人无法通过一个 IP 地址访问到你的服务,所以在例如:微信接口调试、三方对接的时候,你必须将你的服务部署到一个公网的系统中去,这样太累了。 这个时候,内网穿透就出现了,它的作用就是即使你在家的服务,也能被其人访问到。 今天让我们来用一个最简单的案例学习一下如何用 go 来做一个最简单的内网穿透工具。

整体结构 首先我们用几张图来说明一下我们是如何实现的,说清楚之后再来用代码实现一下。

当前网络情况

我们可以看到,画实线的是我们当前可以访问的,画虚线的是我们当前无法进行直接访问的。

我们现在有的路是:

用户主动访问公网服务器是可以的 内网主动访问公网服务也是可以的 当前我们要做的是想办法能让用户访问到内网服务,所以如果能做到公网服务访问到内网服务,那么用户就能间接访问到内网服务了。

想是这么想的,但是实际怎么做呢?用户访问不到内网服务,那我公网服务器同样访问不到吧。所以我们就需要利用现有的链路来完成这件事。

基本架构

内网,客户端(我们要搞一个) 外网,服务端(我们也要搞一个) 访问者,用户 首先我们需要一个控制通道来传递消息,因为只有内网可以访问公网,公网不知道内网在哪里,所以第一次肯定需要客户端主动告诉服务端我在哪 服务端通过 8007 端口监听用户来的请求 当用户发来请求时,服务端需要通过控制信道告诉客户端,有用户来了 客户端收到消息之后建立隧道通道,主动访问服务端的 8008 来建立 TCP 连接 此时客户端需要同时与本地需要暴露的服务 127.0.0.1:8080 建立连接 连接完成后,服务端需要将 8007 的请求转发到隧道端口 8008 中 客户端从隧道中获得用户请求,转发给内网服务,同时将内网服务的返回信息放入隧道 最终请求流向是,如图中的紫色箭头走向,请求返回是如图中红色箭头走向。

需要理解的是,TCP 一旦建立了连接,双方就都可以向对方发送信息了,所以其实原理很简单,就是利用已有的单向路建立 TCP 连接,从而知道对方的位置信息,然后将请求进行转发即可。

代码实现

工具方法 首先我们先定义三个需要使用的工具方法,还需要定义两个消息编码常量,后面会用到

监听一个地址对应的 TCP 请求 CreateTCPListener 连接一个 TCP 地址 CreateTCPConn 将一个 TCP-A 连接的数据写入另一个 TCP-B 连接,将 TCP-B 连接返回的数据写入 TCP-A 的连接中 Join2Conn (别看这短短 10 几行代码,这就是核心了)

客户端 我们先来实现相对简单的客户端,客户端主要做的事情是 3 件:

连接服务端的控制通道 等待服务端从控制通道中发来建立连接的消息 收到建立连接的消息时,将本地服务和远端隧道建立连接(这里就要用到我们的工具方法了)

服务端 服务端的实现就相对复杂一些了:

监听控制通道,接收客户端的连接请求 监听访问端口,接收来自用户的 http 请求 第二步接收到请求之后需要存放一下这个连接并同时发消息给客户端,告诉客户端有用户访问了,赶紧建立隧道进行通信 监听隧道通道,接收来自客户端的连接请求,将客户端的连接与用户的连接建立起来(也是用工具方法)

其他 其中我加入了 keepalive 的消息,用于保持客户端与服务端的一直正常连接 我们还需要定期清理一下服务端 map 中没有建立成功的连接

实验一下 首先在本机用 dokcer 部署一个 nginx 服务(你可以启动一个 tomcat 都可以的),并修改客户监听端口localServerAddr为127.0.0.1:32768,并修改remoteIP 为服务端 IP 地址。然后访问以下,看到是可以正常访问的。

然后编译打包服务端扔到服务器上启动、客户端本地启动,如果控制台输出连接成功,就完成准备了

现在通过访问服务端的 8007 端口就可以访问我们内网的服务了。

遗留问题 上述的实现是一个最小的实现,也只是为了完成基本功能,还有一些遗留的问题等待你的处理:

现在一个客户端连接上了就不能连接第二个了,那怎么做多个客户端的连接呢? 当前这个 map 的使用其实是有风险的,如何做好连接池的管理? TCP 连接的开销是很大的,如何做好连接的复用? 当前是 TCP 的连接,那么如果是 UDP 如何实现呢? 当前连接都是不加密的,如何进行加密呢? 当前的 keepalive 实现很简单,有没有更优雅的实现方式呢? 这些就交给聪明的你来完成了

总结 其实最后回头看看实现起来并不复杂,用 go 来实现已经是非常简单了,所以 github 上面有很多利用 go 来实现代理或者穿透的工具,我也是参考它们抽离了其中的核心,最重要的就是工具方法中的第三个 copy 了,不过其实还有很多细节点需要考虑的。

以上就是Golang实现内网穿透详解的详细内容,更多关于Go内网穿透的资料请关注脚本之家其它相关文章!

Life is fantastic
🥕 More