面试题集锦:浏览器(编辑)

264 阅读20分钟

1、网络中使用最多的图片格式

常见的图片格式有jpg(jpeg), png, gif, webp等(还有bmp、tiff、psd、raw等)。

  • jpeg

    jpeg是有损压缩格式, 不透明,支持不完全读取(可以读取原图、1/2、1/4、1/8大小的图片)。比较适合存储色彩“杂乱”的拍摄图片

    (在保存时有个质量参数[0,100],参数越大图片越保真,体积也越大。一般情况下选择70或80就足够了)。

  • png

    png是一种无损压缩格式,可以有透明效果。比较适合存储几何特征强的图形类图片

  • gif

    gif可以保存多帧图像。gif中有个参数可以控制图片变化的快慢

  • webp

    google开发的一种有损、透明图片格式,相当于jpeg和png的合体,google声称其可以把图片大小减少40%。

2、浏览器缓存机制(客户端缓存有几种方式?浏览器出现 from disk 、 from memory 的策略是啥?)

1. 强缓存

服务器通知浏览器一个缓存时间。在缓存时间内,下次请求,直接用缓存(不再请求);不在时间内,执行比较缓存策略。

Cache-control (相对值) / Expries (绝对值)

  • Expries 是 http1.0 的标准

    let nowTime = new Date ();
    nowTime.setTime( new Date().getTime() + 3600 * 1000); 
    ctx.set("Expires", nowTime.toUТCString());
    
  • 到了 HTTP1.1,Expire 已经被 Cache-Control 替代

    ctx.set("Cache-control","max-age=3600") // 设置缓存时间 3600 s
    
    • public :所有内容都将被缓存 (客户端和代理服务器都可缓存)
    • private :所有内容只有客户端可以缓存Cache-Control 的默认取值
    • no-cache客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
    • no-store :所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
    • max-age=xxx缓存内容将在 xxx 秒后失效
  • Cache-Control 优先级比 Expires 高

  • from memory cache 代表使用内存中的缓存, from disk cache 则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为 memory —> disk (浏览器会根据 css 、脚本、图片 自动分配缓存在哪里)。

2. 协商缓存

让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的 EtagLast-Modified 通过请求发送给服务器,由服务器校验,返回 304 状态码时,浏览器直接使用缓存。出现 from disk 、from memory 的策略是强缓存。

  • Last-Modified / If-Modified-Since

  • Etag / If-None-Match

  • 协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有 Last-Modified / If-Modified-SinceEtag / If-None-Match ,其中 ETag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since

3. 缓存关系

强缓存优于协商缓存强缓存中 Cache—control 优于 Expries协商缓存中 ETag / If-None—Match 优先级高于 Last-Modified / If-Modified-Since

905270529d12fca266115fa68c21e08.jpg

强缓存:

// ...
router.get('/getData', ctx => {
    console.log('request comming...');
    ctx.set("Cache-control","max-age=3600") // 设置缓存时间 3600 s
    
    let res = fs.statSync('./static/index.html')
    let mtime = res.mtime
    ctx.body = {
        name: 'una',
        age: 23
    }
})
// ...

cc7b06c7217419ae37dbd8d5a051ddf.jpg

298078a6bc9b5538112ab721d5783af.jpg

注意不要勾选 Disable cache ,缓存会失效

293b54644190bd326b5cc4466d24e5d.jpg

协商缓存:

// ...
router.get('/getData', ctx => {
    console.log('request comming...');
    ctx.set("Cache-control","no-cache") 
    
    let res = fs.statSync('./static/index.html')
    let mtime = res.mtime
    ctx.set("Last-Modified", mtime) // 记录数据是否有变动(变动时间)
    if(ctx.headers['if-modified-since'] && ctx.headers['if-modified-since']  == mtime){ // 修改时间一致,数据无变动
        ctx.status = 304
    }else {
        ctx.body = {
            name: 'una',
            age: 23
        }
    }
})
// ...

第一次获取时,没有 If-Modified-Since:

1582ce6ddc50e3cbd8b7bec81fa8f0c.jpg

再次获取时,If-Modified-Since 与 Last-Modified 一致,状态码为 304:

311a2306b1fd7342fd3434b455c5107.jpg

分为强缓存协商缓存根据响应的 header 内容来决定

强缓存相关字段有 expires,cache-control。如果cache-control 与expires 同时存在的话,cache-control 的优先级高于expires。协商缓存相关字段有 Last-Modified/If-Modified-Since,Etag/lf-None-Match

2、浏览器输入网址到页面渲染全过程

  • DN
  • 解析 TCP
  • 连接
    • 发送HTTP请求
    • 服务器处理请求并返回HTTP 报文
    • 浏览器解析渲染页面
  • 连接结束

3、一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

分为4个步骤∶

1. 当发送一个 URL 请求时,不管这个 URL 是 Web 页面的 URL 还是 Web 页面上每个资源的 URL,浏览器都会开启个线程来处理这个请求,同时在远程 DNS 服务器上启动一个 DNS 查询。这能使浏览器获得请求对应的 IP 地址。

2. 浏览器与远程 Web 服务器通过 TCP 三次握手协商来建立一个TCP/IP 连接。该握手包括一个同步报文,一个同步-应答报文和一个应答报文,这三个报文在 浏览器和服务器之间传递。该握手首先由客户端尝试建立起通信,然后服务器响应并接受客户端的请求,最后由客户端发出该请求已经被接受的报文。

3. 一旦 TCP/IP 连接建立,浏览器会通过该连接向远程服务器发送HTTP 的 GET 请求。远程服务器找到资源并使用 HTTP响应返回该资源。此时,Web 服务器提供资源服务,客户端开始下载资源。

10、减少页面加载时间的方式

1. 优化图片(图像格式、提供宽高)

(如果浏览器没有找到这两个参数,它需要一边下载图片一边计算大小,如果图片很多,浏览器需要不断地调整页面。这不但影响速度,也影响浏览体验。当浏览器知道了高度和宽度参数后,即使图片暂时无法显示,页面上也会腾出图片的空位,然后继续加载后面的内容。)

2. 优化CSS

3. 减少 http请求

4. 文件压缩(gizp 压缩)

5. 使用 cdn 托管资源

6. 使用缓存

7. meta标签优化(title,description,keywords)、heading标签的优化、alt 优化

8. 反向链接,网站外链接优化

11、HTTP 协议和 HTTPS区别

  • http 是超文本传输协议,信息是明文传输;
  • https是具有安全性的 ssl加密传输协议;

连接方式:

  • http和 https连接方式完全不同,端口也不同,http是80,https是 443 。
  • http 的连接很简单,是无状态的,https协议是由ssl+http 协议构建的,可进行加密传输,身份认证的网络协议,比 http 协议安全。

TCP UDP 协议区别

说下三次握手和四次挥手

image.png

浏览器执行线程

  • 浏览器执行线程

    浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等。

    其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

  • 关于执行中的线程:

    • 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。

    • 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件

http 请求

5、Ajax如何使用

一个完整的AJAX请求包括五个步骤∶

  • 创建XMLHTTPRequest对象

  • 使用open 方法创建 http 请求,并设置请求地址

    xhr.open(get/post,url,isAsync) 经常使用前三个参数

  • 设置发送的数据,用 send 发送请求

  • 注册事件(给 ajax 设置事件)

  • 获取响应并更新页面

4、get 请求传参长度的误区

误区∶我们经常说 get 请求参数的大小存在限制,而 post 请求的参数大小是无限制的。

实际上 HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对 get 请求参数的限制是来源于浏览器或 web 服务器,浏览器或 web 服务器限制了url 的长度。不同的浏览器和 WEB 服务器,限制的最大长度不一样,要支持IE,则最大长度为 2083byte,若只支持Chrome,则最大长度8182byte。

7、get 和 post 请求在缓存方面的区别

  • get 请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。

  • post 不同,post 做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。

因此 get 请求适合于请求缓存。

如何中断 Ajax 请求?

原生里可以通过 XMLHttpRequest 对象上的 abort 方法来中断 Ajax。

注意: abort 方法不能阻止向服务器发送请求,只能停止当前 Ajax 请求。

后端该返回还是返回了,只是前端没有再去接收了。

let xhr = new XMLHttpRequest()
document.querySelector('.startBtn').onClick = function () {
    xhr.open('get', '/api', true)
    xhr.onload = function () {
        console.log(xhr.responseText)
    }
    xhr.send()
}
document.querySelector('.stopBtn').onClick = function () {
    console.log('停止请求')
    xhr.abort()
}

6、常见的http状态码

2XX 成功

  • 200 OK,表示从客户端发来的请求在服务器端被正确处理
  • 202 表示服务器已经接受了请求,但是还没有处理,而且这个请求最终会不会处理还不确定
  • 204 No content,表示请求成功,但响应报文不含实体的主体部分
  • 205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
  • 206 Partial Content,进行范围请求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL
  • 303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
  • 304 not modified,客户端有缓存的文档并发出了一个条件性的请求(一般是提供 If-Modified-Since 头表示客户只想比指定日期更新的文档)。服务器告诉客户端,原来缓存的文档还可以继续使用
  • 307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

4XX 客户端错误

  • 400 bad request,请求报文存在语法错误
  • 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
  • 403 forbidden,表示对请求资源的访问被服务器拒绝
  • 404 not found,表示在服务器上没有找到请求的资源

5XX 服务器错误

  • 500 internal sever error,表示服务器端在执行请求时发生了错误
  • 501 Not lmplemented,表示服务器不支持当前请求所需要的某个功能
  • 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

说一下 CORS 的简单请求和复杂请求的区别?

CORS ( Cross-origin resource sharing ),跨域资源共享,是一份浏览器技术的规范,用来避开浏览器的同源策略。相关头部设置如下:

  • Access-Control-Alow-Origin 指示请求的资源能共享给哪些域
  • Access-Control-Allow-Credentials 指示当请求的凭证标记为 true 时,是否响应该请求
  • Access-Control-Allow-Headers 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。
  • Access-Control-Allow-Methods 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。
  • Access-Control-Expose-Headers 指示哪些 HTTP 头的名称能在响应中列出。
  • Access-Control-Max-Age 指示预请求的结果能被缓存多久
  • Access-Control-Request-Headers 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。
  • Access-Control-Request-Method 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。
  • Origin 指示获取资源的请求是从什么域发起的

CORS 可以分成两种简单请求和复杂请求。

简单请求是满足以下下条件的请求:

  • HTTP 方法是下列之一

    • HEAD
    • GET
    • POST
  • HTTP 头信息不超出以下几种字段

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type ,但仅能是下列之一
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

反之就是复杂请求,复杂请求表面上看起来和简单请求使用上差不多,但实际上浏览器发送了不止一个请求。其中最先发送的是一种 “预请求”,此时作为服务端,也需要返回 “预回应” 作为响应。预请求实际上是对服务端的一种权限请求,只有当预请求成功返回,实际请求才开始执行

// ...
app.use(async (ctx, next) => {
    ctx.set('Access-Control-Alow-Origin', 'http://localhost:3000')
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Language, Authorization, Accept, X-Requseted-With')
    
    if(ctx.method === 'OPTIONS'){
        ctx.body = '' // 随便返回点啥,表示预请求通过
    }else{
        await next()
    }
})

复杂请求,请求2次:

image.png

浏览器为什么要阻止跨域请求?每次跨域请求都需要到达服务端吗?

  • 浏览器阻止跨域请求的原因是“同源策略”,“同源策略”主要解决的问题是浏览器的安全问题。同源是协议、域名、端口都相同,非同源是只要协议、域名、端口有一个不同就会造成非同源。如下:
http://www.baidu.com:443 // http 协议 www.baidu.com 域名 443 端口
http://www.baidu.com:3000

如上两地址由于端口不同就造成跨域问题。非同源会造成:

  1. 无法获取 cookie 、 localstroage 、 indexedDB
  2. 无法访问网页中 dom
  3. 无法发送网络请求

所以浏览器阻止跨域的原因是基于网络安全考虑

跨域是浏览器出于安全策略阻止非同源请求,但是每次跨域请求其实都是正常发送的,服务端也会正常返回,只是被浏览器拦截起来了。所以每次跨域请求都会到达服务端。

如何解决跨域?

解决跨域方式有很多种,例如:

  1. jsonp 跨域
  2. postMessage 解决跨域(iframe onload 之后,通过 iframe.contentWindow.postMessage(JSON.stringify(data), url) 发送数据,对应接收数据的页面,通过监听事件 window.addEventListener('message', e => {console.log(e.data)}) 获取数据)
  3. 跨域资源共享( CORS )(例如: Access-Control-Allow-Origin 设置为 *
  4. nginx 反向代理(服务端,转发)
  5. node.js 中间件正向代理(前端,转发,例如 koa-server-http-proxy
  6. websocket 协议跨域

5、本地存储与cookie 的区别

  • Cookie 非常小,大小限制为 4KB左右。它的主要用途有保存登录信息,比如你登录某个网站市场可以看到"记住密码",这通常就是通过在 Cookie 中存入一段辨别用户身份的数据来实现的。 默认是关闭浏览器后失效

  • localStorage 缓存到本地,除非被清除,否则永久保存。一般为5MB。

  • sessionStorage 会话缓存,页面关闭后被清除

Token 一般存放在哪里?Token 存放在 cookie 和 localStorage、sessionStorage 中有什么不同?

b2d8e76ee0c975caa5c6c8d87d87b1d.jpg

  • Token 其实就是访问资源的凭证。

一般是用户通过用户名和密码登录成功之后,服务器将登陆凭证做数字签名,加密之后得到的字符串作为 token 。

它在用户登录成功之后会返回给客户端,客户端主要有这么几种存储方式:

  1. 存储在 localStorage 中,每次调用接口的时候都把它当成一个字段传给后台;
  2. 存储在 cookie 中,让它自动发送,不过缺点就是不能跨域
  3. 拿到之后存储在 localStorage 中,每次调用接口的时候放在 HTTP 请求头的 Authorization 字段里;

所以 token 在客户端一般存放于 localStorage 、 cookie 或 sessionStorage 中。

  • 将 token 存放在 webStroage 中,可以通过同域的 js 来访问。这样会导致很容易受到 XSS 攻击,特别是项目中引入很多第三方 js 类库的情况下。如果 js 脚本被盗用,攻击者就可以轻易访问你的网站,webStroage 作为一种储存机制,在传输过程中不会执行任何安全标准。

XSS 攻击: cross-site Scripting (跨站脚本攻击)是一种注入代码攻击。恶意攻击者在目标网站上注入 script 代码,当访问者浏览网站的时候通过执行注入的 script 代码达到窃取用户信息,盗用用户身份等。

  • 将 token 存放在 cookie 中可以指定 httponly ,来防止被 Javascript 读取,也可以指定 secure ,来保证 token 只在 HTTPS 下传输。缺点是不符合 Restful 最佳实践,容易受到 CSRF 攻击

CSRF 跨站点请求伪造( Cross-Site Request Forgery ),跟 XSS 攻击一样,存在巨大的危害性。简单来说就是恶意攻击者盗用已经认证过的用户信息,以用户信息名义进行一些操作(如发邮件、转账、购买商品等等)。由于身份已经认证过,所以目标网站会认为操作都是真正的用户操作的。 CSRF 并不能拿到用户信息,它只是盗用的用户凭证去进行操作。

什么是 XSS 攻击?如何防范 XSS 攻击?

答案:XSS 简单点来说,就是攻击者想尽一切办法将可以执行的代码注入到网页中

按照类型来分:XSS 可以分为存储型,反射型及 DOM 型DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞

防止 XSS 攻击可以通过

1、转义字符

如 HTML 元素的编码,JS 编码,CSS 编码,URL 编码等等

  • 避免拼接 HTML
  • Vue/React 技术栈,避免使用 v-html / dangerouslySetinnerHTML

2、增加攻击难度,配置 CSP :

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP :

  • 设置 HTTP Header 中的 Content-Security-Policy。这里以设置 HTTP Header 来举例:

    • 只允许加载本站资源:Content-Security-Policy: default-src 'self'
    • 只允许加载 HTTPS 协议图片:Content-Security-Policy: img-src https://*
    • 允许加载任何来源框架:Content-Security-Po1icy: child-Src 'none'
  • 设置 meta 标签的方式

<meta http-equiv="Content-Security-Policy" content="script-src 'self';object-src 'none'; style-src cdn.example.org; child-src https:">

3、 校验信息

  • 比如一些常见的数字、URL、电话号码、邮箱地址等等做校验判断
  • 开启浏览器 XSS 防御: Http Only cookie,禁止 js 读取某些敏感 cookie,攻击者完成 XSS 注入后也无法窃取此 cookie。
  • 验证码

怎么禁止让 js 读取 Cookie ?怎么让 cookie 只在 HTTPS 下传输?

答案:由于 cookie 会存放在客户端,一般情况下会保存一些凭证及状态信息,为了防止 cookie 泄露造成安全问题。可以设置 cookie 的 httpOnly 属性(默认为 true),那么通过程序(js 脚本、 Applet 等)将无法读取到 Cookie 信息,这样能有效的防止 XSS 攻击。 cookie 中有个属性 secure(默认为 false) ,当该属性设置为 true 时,表示创建的 Cookie 会被以安全的形式向服务器传输,也就是只能在 HTTPS 连接中被浏览器传递到服务器端进行会话验证,如果是 HTTP 连接则不会传递该 cookie 信息,所以不会被窃取到 Cookie 的具体内容。就是只允许在加密的情况下将 cookie 加在数据包请求头部,防止 cookie 被带出来。 secure 属性是防止信息在传递的过程中被监听捕获后信息泄漏。但是这两个属性并不能解决 cookie 在本机出现的信息泄漏的问题

// 后端
router.get('/getUser', ctx => {
    ctx.cookies.set('name', 'cookieName', {
        maxAge: 7200000,
        httpOnly: false, // 默认为 true
        // secure: true // 默认为 false
    })
})

// 前端
console.log(document.cookie)

WebSocket 是怎么实现点对点通信和广播通信的?

答案:WebSocket 是一种全双工通信协议。 WebSocket 让服务端和客户端通信变得简单。最大的特点是可以通过服务端主动推送消息到客户端。前端基于 nodejs 和 WebSocket 实现点对点及广播通信。

  • 广播通信,顾名思义是类似广播一样给多个人进行广播消息。实现广播通信可以使用很多模块,主要能够把流程描述清楚就可以了。我这里采取的是 socket.io 模块。

    • 服务端监听 socket 链接:
    io.on("connection", (socket) => {
        console.log('有 socket 连接')
    })
    
    • 通过监听连接过来的 socket 对象广播对应的信息:
    socket.on("addData", function (data){
        // 广插除了自己之外的其他订阅者
        socket.broadcast.emit("addInputData", data );
    })
    
    • 客户端连接及发送对应的 socket 请求:
    let socket = io.connect ("ws://localhost:3000"); // 连接 socket 服务器 
    socket.emit("addData", JSON.stringify(info)); // 发送 socket 事件
    
  • 点对点通信,顾名思义就是一对一的通信。例如多人实时聊天,可以指定用户来发送消息。点对点通信中需要注意服务端需要记录每个 socket 客户端的连接,需要将客户端及服务端 socket 对象关联起来。广播数据的时候,广播指定对象就可以了。如下:

    • 服务端记录每一个连接过来的 socket 对象,且和用户 id 进行关联:
    socket.on("uid", data => {
        usersObj[data] = socket; //通过 usersObj 来记录连接过来的用户
    })
    
    • 给指定的 socket 对象进行广播:
    socket.on("user", data => {
        let uid = JSON.parse(data).uid;
        usersObj[uid].emit('content', data);
    })
    
    • 客户端监听点对点广播事件:
    socket.on("content", data => {
        console.log(data)
    })
    

总结:WebSocket 区分广播通信及点对点通信,核心在于区分每一个连接的 socket 对象,广播通信需要对于非自身的所有连接的 socket 对象进行通信。而点对点通信,通过关联用户及 socket 对象,且保存每一个 socket 连接,查找指定的 socket 对象,来达到发送指定 socket 连接的目的。

以下是示例代码:

前端:

// 为不同页面设置 uid
let uid;
if(localStorage.getItem('uid')){
    uid = paseInt(localStorage.getItem('uid')) + 1;
    localStorage.setItem('uid', uid);
    document.querySelector('h1').innerHTML = '用户' + uid;
} else {
    localStorage.setItem('uid', 1);
}

let socket = io.connect ("ws://localhost:3000"); // 连接 socket 服务器 
socket.emit("uid", uid); // 发送 uid

// 广播通信
document.querySelector('.btn1').onclick = function () {
    let value = document.querySelector('.textValue').value
    let info = {
        message: value
    }
    socket.emit("addData", JSON.stringify(info)); // 广播信息
}

// 点对点通信
document.querySelector('.btn1').onclick = function () {
    let value = document.querySelector('.textValue').value
    let user = document.querySelector('.user').value // 指定发送对象
    let info = {
        uid: user,
        message: value
    }
    socket.emit("user", JSON.stringify(info)); // 向指定 socket 对象广播信息
}

// 客户端监听点对点广播事件
socket.on("content", data => {
    let p = document.createElement('p')
    p.innerHTML = data
    document.body.appendChild(p)
})

服务端:

// 广播
const Koa = require("koa");
const static = require("koa-static");
const Router = require("koa-router");
let app = new Koa();
let router = new Router();
app.use(static(__dirname + '/static'));
const server = require("http").createServer(app.callback());
const io = require("socket.io")(server);

router.get('/', ctx => {
    ctx.body = 'some value...';
})
app.use(router.routes());

let userObj = {};
io.on("connection", (socket) => {
    console.log('有 socket 连接');
    
    // 广播通信
    socket.on("addData", function (data){
        // broadcast 广插除了自己之外的其他订阅者
        socket.broadcast.emit("addInputData", data );
    })
    
    socket.on("uid", data => {
        usersObj[data] = socket; // 通过 usersObj 来记录连接过来的用户
    })
    
    // 给指定的 socket 对象进行广播
    socket.on("user", data => {
        let uid = JSON.parse(data).uid;
        usersObj[uid].emit('content', data);
    })
})
server.listen(3000)