golang面经

面经

区块脉冲

  1. 自我介绍

  2. 实习时候做的项目的技术栈

  3. 切片扩容机制

    首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量

    否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍

    否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环 增加原来的 1/4, 直到最终容量大于等于新申请的容量

    如果最终容量计算值溢出,则最终容量就是新申请容量

  4. 切片底层字端三个占了多少字节

    24个字节, cap、len、 指向底层数组的指针 各占8字节

  5. channel并发还是原子,怎么实现原子

    原子性、加锁(mutex)

  6. make和new的区别,共同点

    共同点:都在堆上分配内存,都是初始化的操作

    不同点:make针对的是切片,map,channel类型的初始化,返回的是对象实例,new返回的则是指向对象实例的地址的一个指针

  7. context原理,context用来做啥,context在goroutine中的应用

    context 包是 Go 1.7 引入的标准库,主要用于在 goroutine 之间传递取消信号、超时时间、截止时间以及一些共享的值等。它并不是太完美,但几乎成了并发控制超时控制的标准做法。

    使用上,先创建一个根节点的 context,之后根据库提供的四个函数创建相应功能的子节点 context。由于它是并发安全的,所以可以放心地传递。

    当使用 context 作为函数参数时,直接把它放在第一个参数的位置,并且命名为 ctx。另外,不要把 context 嵌套在自定义的类型里。

  8. 死锁四个必要条件

    ​ 1、互斥条件:一个资源每次只能被一个进程使用;

      2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;

      3、不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;

      4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;

  9. 数据库三大范式-索引类型-事务acid怎么实现的

    第一范式:每个列都不可以再拆分。

    第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依 赖于主键的一部分。

    第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他 非主键。

    索引类型(按数据结构):

    1. BTREE b树-用于等值查询与范围查询
    2. HASH hash索引-用于等值查询
    3. FULLTEXT 全文索引-用于搜索
    4. R-Tree 地理空间数据-空间类型数据

    MySQL事务机制的核心是两个日志文件:

    redo log(重做日志) undo log(回滚日志) redo log

    redo log主要实现的是事务中的持久性,记录物理数据变化即DML操作

    redo log分为两部分:redo log file 和redo log buffer ,redo log file负责将内容存储到磁盘,redo log buffer负责将内容加载到内存

    重要概念:日志先行

    undo log undo log主要实现的是事务中的原子性,负责日志的回滚操作,并实现MVCC

    如何回滚?

    答:执行逆向SQL语句实现回滚操作,比如操作的语句是insert into …,回滚时undo log就会执行delete form …

  10. 触发器、触发器应用场景

    MySQL从 5.0.2 版本开始支持触发器。MySQL的触发器和存储过程一样,都是嵌入到MySQL服务器的一 段程序。

    触发器是由 事件来触发 某个操作,这些事件包括 INSERT 、 UPDATE 、 DELETE 事件。所谓事件就是指 用户的动作或者触发某项行为。如果定义了触发程序,当数据库执行这些语句时候,就相当于事件发生 了,就会 自动 激发触发器执行相应的操作。

    当对数据表中的数据执行插入、更新和删除操作,需要自动执行一些数据库逻辑时,可以使用触发器来 实现。

    触发器可以确保数据的完整性。

    触发器可以帮助我们记录操作日志。

    触发器还可以用在操作数据前,对数据进行合法性检查。

  11. gmp模型、工作机制、谁先绑定谁

    G(Goroutine):我们所说的协程,为用户级的轻量级线程,每个 Goroutine 对象中的 sched 保存着其上下文信息

    M(Machine):对内核级线程的封装,数量对应真实的 CPU 数(真正干活的对 象)。

    P(Processor):即为 G 和 M 的调度对象,用来调度 G 和 M 之间的关联关系, 其数量可通过 GOMAXPROCS()来设置,默认为核心数。

    每个 P 有个局部队列,局部队列保存待执行的 goroutine,当 M 绑定的 P 的的局部队列已经满了之后就会把 goroutine 放到全局队列

    每个 P 和一个 M 绑定,M 是真正的执行 P 中 goroutine 的实体, M 从绑定的 P 中的局部队列获取 G 来执行

    当 M 绑定的 P 的局部队列为空时,M 会从全局队列获取到本地队列来执行 G ,当从全局队列中没有获取到可执行的 G 时候,M 会从其他 P 的局部队列中偷取 G 来执行,这种从其他 P 偷的方式称为 work stealing

    当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找新的 idle 的 M,若没有 idle 的 M 就会新建一个 M

    当 G 因 channel 或者 network I/O 阻塞时,不会阻塞 M,M 会寻找其他 runnable 的 G;当阻塞的 G 恢复后会重新进入 runnable 进入 P 队列等待执 行

  12. gc原理

    原理:

    1、首先把所有的对象都放到白色的集合中

    2、从根节点开始遍历对象,遍历到的白色对象从白色集合中放到灰色集合中

    3、遍历灰色集合中的对象,把灰色对象引用的白色集合的对象放入到灰色集 合中,同时把遍历过的灰色集合中的对象放到黑色的集合中

    4、循环步骤 3,知道灰色集合中没有对象

    5、步骤 4 结束后,白色集合中的对象就是不可达对象,也就是垃圾,进行回 收

  13. 对象大内存小内存微内存根据多少内存大小来划分

    Go的内存分配分为微内存分配,小内存分配,大内存分配,微内存为小于16字节的内存分配,小内存则为大于16字节小于32KB的内存分配,大内存是大于32KB的内存分配。

深信服

https://exam.nowcoder.com/cts/17268055/summary?id=E012FF3B697488925D2B1AAD67C4DCC3

前海汇流

  1. 假设访问百度这个网站,输了网址,到看到整个网站展示出来,整个过程发生了哪些事情。 ✔

    • DNS 解析:将域名解析成 IP 地址

    • TCP 连接:TCP 三次握手

    • 发送 HTTP 请求

    • 服务器处理请求并返回 HTTP 报文

    • 浏览器解析渲染页面

    • 断开连接:TCP 四次挥手

      域名解析(DNS) 在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。

      浏览器如何通过域名去查询 URL 对应的 IP 呢?

      浏览器缓存 – 浏览器会缓存DNS记录一段时间。 (2分钟到30分钟不等)。

      系统缓存 – 如果在浏览器缓存里没有找到需要的记录,浏览器会做一个系统调用(windows里是gethostbyname)。

      路由器缓存 – 接着,前面的查询请求发向路由器,它一般会有自己的DNS缓存。

      ISP DNS 缓存 – ISP 有专门的 DNS 服务器应对 DNS 查询请求。

      递归搜索 – 你的ISP的DNS服务器从根域名服务器开始进行递归搜索,从.com顶级域名服务器到baidu的域名服务器。一般DNS服务器的缓存中会有.com域名服务器中的域名,所以到顶级服务器的匹配过程不是那么必要了。

      浏览器通过向 DNS 服务器发送域名,DNS 服务器查询到与域名相对应的 IP 地址,然后返回给浏览器,浏览器再将 IP 地址打在协议上,同时请求参数也会在协议搭载,然后一并发送给对应的服务器。接下来介绍向服务器发送 HTTP 请求阶段,服务器返回页面资源给浏览器,浏览器解析渲染页面。浏览器解析渲染页面分为一下五个步骤:

      • 根据 HTML 解析出 DOM 树
      • 根据 CSS 解析生成 CSS 规则树
      • 结合 DOM 树和 CSS 规则树,生成渲染树
      • 根据渲染树计算每一个节点的信息
      • 根据计算好的信息绘制页面
  2. DNS服务器作用。 ✔

    在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。

  3. 访问百度用的网络协议。https ✔

  4. https如何通讯。https工作机制。 ✔

    三次握手四次挥手

    1、客户端发起 HTTPS 请求

    用户在浏览器里输入一个 https 网址,然后连接到 server 的 443 端口。

    2、传送证书

    采用 HTTPS 协议的服务器必须要有一套数字证书,这套证书其实就是一对公钥和私钥,公钥就像是一把锁头,私钥是一个钥匙,只有这个私钥才能打开公钥,其他人打不开自然就看不到里边的内容了

    服务器将证书发送给客户端,这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。

    3、客户端解析证书

    这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,如果发现异常,则会弹出一个警告框,提示证书存在问题。

    4、生成随机密钥

    如果证书没有问题,那么就生成一个随机对称密钥 5、加密对称密钥

    公钥对该对称密钥进行加密,就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。

    6、传送加密信息

    将加密后的对称密钥发送给服务器,目的就是让服务端得到对称密钥,以后客户端和服务端的通信就可以通过这个对称密钥进行加密解密了。

    7、服务端解密信息

    服务端用私钥解密后,得到了客户端传过来的对称密钥,然后把内容通过该密钥进行对称加密,所谓对称加密就是,将信息和对称密钥通过某种算法混合在一起,这样除非知道对称密钥,不然无法获取内容,而正好客户端和服务端都知道这个密钥,所以只要加密算法够彪悍,对称密钥够复杂,数据就够安全。

    8、传输加密后的信息

    这部分信息是服务器用对称密钥加密后的信息,可以在客户端被还原。

    9、客户端解密信息

    客户端用之前生成的对称密钥解密服务段传过来的信息,于是获取了解密后的内容,整个过程第三方即使监听到了数据,也束手无策。

  5. 对称加密和非对称加密,整个https流程为什么要通过两种加密方式来做呢 ×

    对称加密

    对称加密指的是加密和解密用同一个密钥。但是在通信之前,客户端和服务端是不会有这样同一把密钥的。需要其中一方将密钥发送给对方。在整个传输过程没有任何验证操作,所以黑客也可以截取到这把密钥从而破译出加密的内容。所以纯对称加密是不安全的。

    非对称加密

    非对称加密指的是加密和解密用不同的密钥。可以是用私钥加密,公钥解密,也可以是用公钥加密,私钥解密。但是会有这种情况。服务端拥有私钥和公钥,将公钥发给客户端。既然客户端可以获得公钥,黑客也可以获得公钥。那么服务端发送给客户端的所有内容黑客也是可以解读的。客户端用公钥加密发送给服务端不受影响,因为黑客手上没有服务器的私钥。所以纯非对称加密也是不安全的。

  6. 对称加密和非对称加密有什么样的差异。 ×

    对称加密

    对称加密指的是加密和解密用同一个密钥。但是在通信之前,客户端和服务端是不会有这样同一把密钥的。需要其中一方将密钥发送给对方。在整个传输过程没有任何验证操作,所以黑客也可以截取到这把密钥从而破译出加密的内容。所以纯对称加密是不安全的。

    非对称加密

    非对称加密指的是加密和解密用不同的密钥。可以是用私钥加密,公钥解密,也可以是用公钥加密,私钥解密。但是会有这种情况。服务端拥有私钥和公钥,将公钥发给客户端。既然客户端可以获得公钥,黑客也可以获得公钥。那么服务端发送给客户端的所有内容黑客也是可以解读的。客户端用公钥加密发送给服务端不受影响,因为黑客手上没有服务器的私钥。所以纯非对称加密也是不安全的。

  7. 响应码。502和503的区别 ×

    常见的返回请求状态码:

    200:客户端请求成功。 201:表示请求已经被成功处理,并且创建了新的资源。新的资源在响应返回之前已经被创建。

    301:表示资源已经永久移动到另一个位置。 302:临时重定向,表示资源临时移动到了另一个位置。 304:表示客户端可以使用以前请求的结果,不需要再次请求。此特性可以节省服务器流量,还可以加速客户端访问。

    400:表示由于语法无效,服务器无法理解该请求。客户端不应该在未经修改的情况下重复此请求。 401:请求未经授权,这个状态代码必须和WWW-Authenticate报头字段一起使用,一般属于客户端调用问题,但也可能是服务器端设置有问题。 403:指的是服务器端有能力处理该请求,但是拒绝授权访问。 404:请求资源不存在,比如资源被删除了,或用户输入了错误的URL。

    500:服务器发生不可预期的错误,一般是代码的BUG所导致的。 502:表示作为网关或代理角色的服务器,从上游服务器(如tomcat、php-fpm)中接收到的响应是无效的。例如Nginx+uWSGI,当服务没有启动成功或异常退出,而Nginx服务是正常的情况下,就会看到 502:Bad Gateway(无效网关)错误。 503:服务器当前不能处理客户端的请求,一段时间后可能恢复正常 504:表示扮演网关或者代理的服务器无法在规定的时间内获得想要的响应。

  8. 数据库中有以下两张数据表: 
    
    广告表
    表名:ad
    字段:id, title, create_time
    
    订单表
    表名:order
    字段:id, ad_id, cost, create_time
    
    请写一条 SQL,查询订单数量前十的广告ID、广告名称、订单数量、订单费用。基于写的sql语句,设计一下两张表的索引。 
    
    select ad.id,ad.title,count(select * from ad,order where ad.id = order.ad_id) as order_count,order.cost from ad,order where ad.id = order.ad_id order by order_count desc limit 10 ×
    
    
    根据你的需求,我可以提供以下 SQL 查询语句来获取订单数量前十的广告ID、广告名称、订单数量、订单费用:
    
    SELECT ad.id, ad.title, COUNT(order.id) AS order_count, SUM(order.cost) AS total_cost
    FROM ad
    JOIN `order` ON ad.id = `order`.ad_id
    GROUP BY ad.id, ad.title
    ORDER BY order_count DESC
    LIMIT 10;
    
    
    这个查询语句使用了 JOIN 操作符将 ad 和 order 两张表连接起来,连接条件是 ad 表的 id 列和 order 表的 ad_id 列相等。然后,它按照 ad.id 和 ad.title 列进行分组,计算每个广告的订单数量和订单费用总额。最后,按照订单数量降序排序,并限制结果集为前十个结果。
    
    需要注意的是,GROUP BY 子句是必要的,以确保聚合函数(COUNT 和 SUM)的计算正确。此外,如果有多个广告的订单数量相同,查询结果将返回所有这些广告的信息。
    
    为了优化之前提供的 SQL 查询语句的性能,我们可以为 ad 和 order 两张表添加索引。
    
    对于 ad 表,我们可以在 id 列上添加索引,因为它在连接条件中使用:
    
    ALTER TABLE ad ADD INDEX idx_ad_id (id);
    
    
    对于 order 表,我们可以在 ad_id 和 title 列上添加索引,因为它们分别在连接条件和 GROUP BY 和 ORDER BY 子句中使用:
    
    ALTER TABLE `order` ADD INDEX idx_order_ad_id_create_time (ad_id, title);
    
    
    这些索引应该通过允许数据库快速定位每个表中的相关行来提高查询性能。但是,需要注意的是,添加过多的索引实际上可能会损害性能,因此需要根据运行的具体查询仔细考虑哪些列需要添加索引。
    

10.数据库的索引,了解过怎么样实现的。B+树 ✔

  1. B+树非叶子节点上是不存储数据的,仅存储键值,而B树节点中不仅存储键值,也会存储数据。之所以这么做是因为在数据库中页的大小是固定的,innodb中页的默认大小是16KB。如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快。另外,B+树的阶数是等于键值的数量的,如果我们的B+树一个节点可以存储1000个键值,那么3层B+树可以存储1000×1000×1000=10亿个数据。一般根节点是常驻内存的,所以一般我们查找10亿数据,只需要2次磁盘IO。

  2. 因为B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而B树因为数据分散在各个节点,要实现这一点是很不容易的。

    有心的读者可能还发现上图B+树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。

    其实上面的B树我们也可以对各个节点加上链表。其实这些不是它们之前的区别,是因为在mysql的innodb存储引擎中,索引就是这样存储的。也就是说上图中的B+树索引就是innodb中B+树索引真正的实现方式,准确的说应该是聚集索引(聚集索引和非聚集索引下面会讲到)。

11.Mysql索引的聚簇索引和非聚簇索引的差异。✔

聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据

非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数 据的对应行,myisam 通过 key_buffer 把索引先缓存到内存中,当需要访问 数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找 到磁盘相应数据,这也就是为什么索引不在 key buffer 命中时,速度慢的 原因。

  1. ×
函数 reverse (char *s, int len) 的功能是用递归的方式逆置长度为len的字符串s。例如:若s的内容为“abcd”,则逆置后其内容变为“dcba”,请补充完整下面的代码。

void reverse (char *s, int len)
{
char ch;
if (____1____)
{
ch = *s;
*s = * (s + len - 1);
* (s + len - 1) = ch;
reverse (____2____, ____3____);
}
}

人人租(拿到offer)

  1. 设计模式 (单例✔、代理✔)

    其他类型的模式也要熟悉,工厂设计模式

    (13条消息) Go 学习笔记(90)— 常用设计模式(单例模式、工厂模式、策略模式、模板模式、代理模式、选项模式)_模板模式,策略模式与工厂模式的区别是什么意思-CSDN博客

  2. 知乎DDD领域

    领域驱动设计(DDD)靠谱吗? - 知乎 (zhihu.com)

  3. Go option选项模式

    (13条消息) go使用options模式设置参数_go option_CK持续成长的博客-CSDN博客

  4. 为什么append要赋值给原切片的变量

    append之后可能会引发切片底层扩容,从而分配新的内存地址,所以要重新赋值一下

    因为当底层数组的空间不足的时候 , 会扩充内存空间 ,内存空间会重新分配,通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量

  5. zset的底层实现

    (13条消息) redis跳表——zset的底层实现_redis zset 跳表_李歘歘的博客-CSDN博客

  6. channel的底层实现

    深入分析Go1.18 Channel底层原理 - 腾讯云开发者社区-腾讯云 (tencent.com)

铁威马

题目

两台Linux系统设备A(10.18.13.100)和B(10.18.13.101),我希望在A的浏览器输入http://127.0.0.1:8855 浏览器里面显示:“http B mac地址是:xxxxx”(xxxx是B设备的第一个网卡的mac地址。)在A的浏览器输入https://127.0.0.1:8854 浏览器里面显示 “https B mac地址是:xxxxx”。A和B的之间用9222端口tcp连接且仅B上面有web服务。本质上面就是A机器里面有个代理的客户端,把http 和https 两个应用协议通过 9222 port的代理到B机器上面,B上面把http的web请求通过9222的端口回复给A。

解答:

Golang 实现的代码。需要注意的是,这个代码需要在设备 B 上运行,因为只有设备 B 上有 Web 服务。代码中使用了 net/httpnet/http/httputil 两个包来实现代理功能。

package main

import (
    "fmt"
    "net"
    "net/http"
    "net/http/httputil"
)

func main() {
    // 获取设备 B 的第一个网卡的 MAC 地址
    ifas, err := net.Interfaces()
    if err != nil {
        panic(err)
    }
    var mac net.HardwareAddr
    for _, ifa := range ifas {
        // 忽略回环接口和未启用的接口
        if ifa.Flags&net.FlagUp != 0 && ifa.Flags&net.FlagLoopback == 0 {
            mac = ifa.HardwareAddr
            break
        }
    }
    if mac == nil {
        panic("cannot get mac address")
    }

    // 定义 HTTP 和 HTTPS 的代理函数
    proxyHTTP := func(w http.ResponseWriter, req *http.Request) {
        // 在响应中返回设备 B 的第一个网卡的 MAC 地址
        fmt.Fprintf(w, "http B mac地址是:%s\n", mac)
        // 定义一个 Director 函数,用于修改请求的 URL
        director := func(req *http.Request) {
            req.URL.Scheme = "http"
            req.URL.Host = "127.0.0.1:80"
        }
        // 创建一个 ReverseProxy 对象并调用 ServeHTTP 方法,将请求转发到设备 B 上的 Web 服务
        proxy := &httputil.ReverseProxy{Director: director}
        proxy.ServeHTTP(w, req)
    }
    proxyHTTPS := func(w http.ResponseWriter, req *http.Request) {
        // 在响应中返回设备 B 的第一个网卡的 MAC 地址
        fmt.Fprintf(w, "https B mac地址是:%s\n", mac)
        // 定义一个 Director 函数,用于修改请求的 URL
        director := func(req *http.Request) {
            req.URL.Scheme = "https"
            req.URL.Host = "127.0.0.1:443"
        }
        // 创建一个 ReverseProxy 对象并调用 ServeHTTP 方法,将请求转发到设备 B 上的 Web 服务
        proxy := &httputil.ReverseProxy{Director: director}
        proxy.ServeHTTP(w, req)
    }

    // 启动 HTTP 和 HTTPS 服务器
    http.HandleFunc("/", proxyHTTP)
    http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) {})
    go http.ListenAndServe(":80", nil) // 监听 80 端口并接收 HTTP 请求
    go http.ListenAndServeTLS(":443", "server.crt", "server.key", nil) // 监听 443 端口并接收 HTTPS 请求

    // 监听 9222 端口并接收连接
    listener, err := net.Listen("tcp", "127.0.0.1:9222")
    if err != nil {
        panic(err)
    }
    for {
        conn, err := listener.Accept() // 接收连接
        if err != nil {
            panic(err)
        }
        go func() {
            defer conn.Close()
            buf := make([]byte, 1024)
            n, err := conn.Read(buf) // 读取请求
            if err != nil {
                return
            }
            req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(buf[:n]))) // 解析请求
            if err != nil {
                return
            }
            if req.Method == "GET" && req.URL.Path == "/http" {
                proxyHTTP(conn, req) // 转发 HTTP 请求
            } else if req.Method == "GET" && req.URL.Path == "/https" {
                proxyHTTPS(conn, req) // 转发 HTTPS 请求
            }
        }()
    }
}

这个代码会同时启动 HTTP 和 HTTPS 服务器,并监听 9222 端口。当设备 A 通过 9222 端口发送 HTTP 或 HTTPS 请求时,代码会将请求转发到设备 B 上的 Web 服务,并在响应中返回设备 B 的第一个网卡的 MAC 地址。

根据要求,这个系统需要在设备 A 上启动一个代理客户端,将设备 A 发送的 HTTP 和 HTTPS 请求转发到设备 B 上的 Web 服务。同时,在设备 A 的浏览器中输入 http://127.0.0.1:8855 时,应该在浏览器中显示 "http B mac地址是:xxxxx",其中 "xxxxx" 是设备 B 的第一个网卡的 MAC 地址。在设备 A 的浏览器中输入 https://127.0.0.1:8854 时,应该在浏览器中显示 "https B mac地址是:xxxxx"。

为了实现这个功能,需要在启动一个 HTTP 和 HTTPS 服务器,监听 80 和 443 端口,分别接收设备 A 发送的 HTTP 和 HTTPS 请求,并将请求转发到设备 B 上的 Web 服务。将设备 A 的 HTTP 和 HTTPS 请求转发到设备 B 上的 HTTP 和 HTTPS 服务器。B在处理相应的时候,需要在响应中附加设备 B 的第一个网卡的 MAC 地址,以便在设备 A 的浏览器中显示。

以下是做了一些优化并符合 Go 语言的编码风格的代码:

package main

import (
	"bufio"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"net/http/httptest"
	"net/http/httputil"
)

const (
	httpPort  = ":8855"
	httpsPort = ":8854"
	proxyPort = ":9222"
)

func main() {
	// 获取设备 B 的第一个网卡的 MAC 地址
	mac, err := getFirstInterfaceMAC()
	if err != nil {
		panic(err)
	}

	// 定义 HTTP 和 HTTPS 的代理函数
	proxyHTTP := func(w http.ResponseWriter, req *http.Request) {
		// 在响应中返回设备 B 的第一个网卡的 MAC 地址
		fmt.Fprintf(w, "http B mac地址是:%s\n", mac)
		// 创建一个 ReverseProxy 对象并调用 ServeHTTP 方法,将请求转发到设备 B 上的 Web 服务
		proxy := &httputil.ReverseProxy{Director: func(req *http.Request) {
			req.URL.Scheme = "http"
			req.URL.Host = "127.0.0.1:80"
		}}
		proxy.ServeHTTP(w, req)
	}
	proxyHTTPS := func(w http.ResponseWriter, req *http.Request) {
		// 在响应中返回设备 B 的第一个网卡的 MAC 地址
		fmt.Fprintf(w, "https B mac地址是:%s\n", mac)
		// 创建一个 ReverseProxy 对象并调用 ServeHTTP 方法,将请求转发到设备 B 上的 Web 服务
		proxy := &httputil.ReverseProxy{Director: func(req *http.Request) {
			req.URL.Scheme = "https"
			req.URL.Host = "127.0.0.1:443"
		}}
		proxy.ServeHTTP(w, req)
	}

	// 启动 HTTP 和 HTTPS 服务器
	http.HandleFunc("/", proxyHTTP)
	go http.ListenAndServe(httpPort, nil)
	go http.ListenAndServeTLS(httpsPort, "server.crt", "server.key", nil)

	// 监听 9222 端口并接收连接
	listener, err := net.Listen("tcp", proxyPort)
	if err != nil {
		panic(err)
	}
	defer listener.Close()
	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		go handleConnection(conn, proxyHTTP, proxyHTTPS)
	}
}

func getFirstInterfaceMAC() (string, error) {
	ifas, err := net.Interfaces()
	if err != nil {
		return "", err
	}
	for _, ifa := range ifas {
		if ifa.Flags&net.FlagUp != 0 && ifa.Flags&net.FlagLoopback == 0 {
			return ifa.HardwareAddr.String(), nil
		}
	}
	return "", fmt.Errorf("cannot get mac address")
}

func handleConnection(conn net.Conn, proxyHTTP, proxyHTTPS http.HandlerFunc) {
	defer conn.Close()
	// 读取 HTTP 请求
	req, err := http.ReadRequest(bufio.NewReader(conn))
	if err != nil {
		return
	}

	// 判断请求方法和路径
	if req.Method == http.MethodGet && req.URL.Path == "/http" {
		// 将 conn 对象包装为 http.ResponseWriter 对象
		w := httptest.NewRecorder()
		// 创建一个 http.Request 对象并将其传递给 proxyHTTP 函数
		proxyHTTP(w, req)
		// 将响应写回到 conn 对象
		w.Result().Write(conn)
	} else if req.Method == http.MethodGet && req.URL.Path == "/https" {
		// 将 conn 对象包装为 http.ResponseWriter 对象
		w := httptest.NewRecorder()
		// 创建一个 http.Request 对象并将其传递给 proxyHTTPS 函数
		proxyHTTPS(w, req)
		// 将响应写回到 conn 对象
		w.Result().Write(conn)
	}
}

func init() {
	// 设置全局的 TLS 配置,忽略证书校验
	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}

这个代码和之前的代码差别不大,主要做了以下几个优化:

  • 使用常量来定义端口号,使得代码更容易维护和修改。
  • 将获取设备 B 的第一个网卡的 MAC 地址的代码封装成一个函数,使得代码更清晰和易读。
  • 将接收连接和处理连接的代码封装成一个独立的函数,并使用 defer 关键字来确保在函数退出时关闭连接。
  • 使用 http.MethodGet 常量来代替字符串 "GET",使得代码更可读和更易维护。
  • 在初始化函数中设置全局的 TLS 配置,忽略证书校验,以便在开发时使用自签名证书。

要测试这个代理服务器,你需要在本地环境中运行该程序,并执行以下步骤:

  1. 在设备 B 上启动一个 Web 服务器,监听 80 和 443 端口,并提供一个简单的网页,以便在浏览器中访问。
  2. 在设备 A 的浏览器中输入 http://127.0.0.1:8855,应该在浏览器中显示 "http B mac地址是:xxxxx",其中 "xxxxx" 是设备 B 的第一个网卡的 MAC 地址。
  3. 在设备 A 的浏览器中输入 https://127.0.0.1:8854,应该在浏览器中显示 "https B mac地址是:xxxxx",其中 "xxxxx" 是设备 B 的第一个网卡的 MAC 地址。
  4. 在设备 A 的终端中使用 curl 或者类似的工具发送 HTTP 请求到代理服务器,例如:
curl http://127.0.0.1:9222/http

代理服务器会将请求转发到设备 B 上的 HTTP 服务器,并在响应中附加设备 B 的第一个网卡的 MAC 地址。

  1. 在设备 A 的终端中使用 curl 或者类似的工具发送 HTTPS 请求到代理服务器,例如:
curl --insecure https://127.0.0.1:9222/https

代理服务器会将请求转发到设备 B 上的 HTTPS 服务器,并在响应中附加设备 B 的第一个网卡的 MAC 地址。

通过以上测试,你可以验证代理服务器是否正确工作,并且能够将请求转发到设备 B 上的 Web 服务器,并在响应中附加设备 B 的第一个网卡的 MAC 地址。

可以使用 OpenSSL 生成自签名证书:

  1. 生成私钥文件 server.key
openssl genrsa -out server.key 2048
  1. 生成证书签名请求文件 server.csr
openssl req -new -key server.key -out server.csr

在运行上述命令时,需要填写一些证书信息,如国家、省份、城市、单位、域名等。在填写域名时,请确保填写的是你要使用的域名或 IP 地址。

  1. 使用私钥文件和签名请求文件生成证书文件 server.crt
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

在运行上述命令时,-days 参数指定证书的有效期,可以根据需要进行调整。执行完上述命令后,将会生成一个自签名证书 server.crt,可以将其用于测试或开发环境中。

请注意,自签名证书不受信任,因此在使用时可能会收到浏览器或客户端的安全警告。在生产环境中,应该使用由受信任的证书颁发机构 (CA) 签发的证书,以获得更高的安全性和可信度。

package main

import (
	"bytes"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
)

func main() {
	// 创建一个 ReverseProxy 对象,并将其设置为代理服务器的目标地址
	proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: "127.0.0.1:9222"})

	// 创建一个 HTTP 服务器并监听 8855 端口
	//http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	//	// 响应 HTTP 请求
	//	fmt.Fprint(w, "Hello, HTTP!\n")
	//})
	go func() {
		if err := http.ListenAndServe(":8855", proxyHTTP(proxy)); err != nil {
			log.Fatal(err)
		}
	}()

	//// 创建一个 HTTPS 服务器并监听 8854 端口
	//http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
	//	// 响应 HTTPS 请求,并将设备 B 的第一个网卡的 MAC 地址包含在响应中
	//	fmt.Fprintf(w, "Hello, HTTPS! MAC Address: %s\n", getMACAddress())
	//})
	go func() {
		if err := http.ListenAndServeTLS(":8854", "server.crt", "server.key", proxyHTTPS(proxy)); err != nil {
			log.Fatal(err)
		}
	}()

	// 无限循环,等待信号
	select {}
}

func proxyHTTP(proxy *httputil.ReverseProxy) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// 在响应中包含设备 B 的第一个网卡的 MAC 地址
		fmt.Fprintf(w, "Hello, HTTPS! MAC Address: %s\n", getMACAddress())
	}
}

func proxyHTTPS(proxy *httputil.ReverseProxy) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// 在响应中包含设备 B 的第一个网卡的 MAC 地址
		fmt.Fprintf(w, "Hello, HTTPS! MAC Address: %s\n", getMACAddress())
	}
}

func getMACAddress() string {
	ifas, err := net.Interfaces()
	if err != nil {
		log.Fatal(err)
	}
	for _, ifa := range ifas {
		if ifa.Flags&net.FlagUp != 0 && bytes.Compare(ifa.HardwareAddr, nil) != 0 {
			return ifa.HardwareAddr.String()
		}
	}
	return "unknown"
}

豹亮科技

  1. 自我介绍

  2. 实习做的项目,系统,数据量大不大

  3. MYSQL的存储引擎,innodb和mylsam的区别

    区别: 1、 InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务; 2、InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败; 3、nnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。

    4、MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

    5、也就是说:InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而MyISAM的B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针。

    6、InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁

    7、InnoDB的行锁是实现在索引上的,而不是锁在物理行记录上。潜台词是,如果访问没有命中索引,也无法使用行锁,将要退化为表锁。

    8、innoDB表必须有唯一索引(如主键)(用户没有指定的话会自己找/生产一个隐藏列Row_id来充当默认主键),而Myisam可以没有

    为什么myisam查询比innodb快? 主要原因有三点:

    1. 表锁设计:MyISAM使用的是表级别的锁(table-level locking),而InnoDB使用的是行级别的锁(row-level locking)和MVCC(多版本并发控制)。在执行大量读取操作的情况下,MyISAM可能会更快,因为表锁对读操作的开销较小。然而,如果存在大量的写操作,或者读写混合的负载,InnoDB的行锁可能会提供更好的性能。

    2. 数据存储和索引结构:MyISAM将数据和索引存储在两个独立的文件中,而InnoDB则将数据和索引存储在同一个表空间。这意味着MyISAM可以更快地读取索引,因为它不需要在数据和索引之间进行切换。

    3. 缓存:MyISAM只缓存索引数据,而InnoDB既缓存索引数据,也缓存实际的行数据。如果查询主要基于索引,并且可用内存相对较少,MyISAM可能会提供更好的性能。

    4. MyISAM 引擎使用表级锁定而不是行级锁定,在并发查询较多的情况下,减少了锁定的资源竞争和系统开销。

    5. MyISAM 引擎不支持事务处理,因此避免了事务处理带来的额外性能损耗。

    6. MyISAM 引擎对于只读或者很少写入的表具有更高的查询性能,因为它在数据文件中存储了数据行的物理位置,可以更快地进行磁盘扫描操作。

    7. MyISAM 引擎使用压缩技术来减小数据文件大小,从而减少磁盘IO操作和内存占用。

    然而,虽然MyISAM在某些情况下可能提供更快的查询性能,但它也有一些重要的限制,如不支持事务,不支持行级锁定,以及崩溃后的恢复能力较差。因此,对于需要高并发写入,事务支持,或者更好的崩溃恢复能力的应用,InnoDB可能是更好的选择。

  4. 如果sql慢,怎么排查解决

    第1个原因:没有索引或者 导致索引失效

    第2个原因:单表数据量数据过多,导致查询瓶颈

    第3个原因:网络原因或者机器负载过高。

    第4个原因:热点数据导致单点负载不均衡。

    **第1种情况:**索引失效或者没有没有索引的情况

    首先,可以打开MySQL的慢查询日志,收集一段时间的慢查询日志内容,然后找出耗时最长的SQL语句,对这些SQL语句进行分析。

    比如可以利用执行计划explain去查看SQL是否有命中索引。如果发现慢查询的SQL没有命中索引,可以尝试去优化这些SQL语句,保证SQL走索引执行。如果SQL结构没有办法优化的话,可以考虑在表上再添加对应的索引。我们在优化SQL或者是添加索引的时候,都需要符合最左匹配原则。

    **第2种情况:**单表数据量数据过多,导致查询瓶颈的情况。即使SQL语句走了索引,表现性能也不会特别好。这个时候我们需要考虑对表进行切分。表切分规则一般分为两种,一种是水平切分,一种是垂直切分。

    水平切分的意思是把一张数据行数达到千万级别的大表,按照业务主键切分为多张小表,这些小表可能达到100张甚至1000张。

    **第3种情况:**网络原因或者机器负载过高的情况,我们可以进行读写分离.

    比如MySQL支持一主多从的分布式部署,我们可以将主库只用来处理写数据的操作,而多个从库只用来处理读操作。在流量比较大的场景中,可以增加从库来提高数据库的负载能力,从而提升数据库的总体性能。

    **第4种情况:**热点数据导致单点负载不均衡的情况。

    这种情况下,除了对数据库本身的调整以外,还可以增加缓存。将查询比较频繁的热点数据预存到缓存当中,比如Redis、MongoDB、ES等,以此来缓解数据的压力,从而提高数据库的响应速度。

手游科技

  1. panic怎么捕获,保证高可用

    使用recover()函数

  2. goroutine怎么让出

    动图图解!怎么让goroutine跑一半就退出? - golang小白成长记 - SegmentFault 思否

  • 通过 runtime.Goexit()可以做到提前结束协程,且结束前还能执行到defer的内容
  • runtime.Goexit()其实是对goexit0的封装,只要执行 goexit0 这个函数,当前协程就会退出,同时还能调度下一个可执行的协程出来跑。
  • 通过newproc可以创建出新的goroutine,它会在函数栈底部插入一个goexit。
  • os.Exit() 指的是整个进程退出;而runtime.Goexit()指的是协程退出。两者含义有区别。
  1. 实习做的项目、负责的模块、项目人员规模

  2. 切片、Map

  3. channel

  4. Redis

  5. 如何理解go的多态机制

    Golang函数的多态和封装的实现和底层原理-Golang-PHP中文网

  6. 讲讲Mysql的索引

  7. 用explain语句解析sql语句的时候,遇到什么情况需要继续优化

    MySQL高级:explain分析SQL,索引失效&常见优化场景_explain sql 索引_Java架构师公社的博客-CSDN博客

    当Type字段显示为all的时候,将遍历全表以找到匹配的行,此时需要优化,让sql语句走索引。

end
  • 作者:AWhiteElephant(联系作者)
  • 发表时间:2023-09-07 12:46
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 评论