最新文章
  • javascript之dom

    javascript之dom

    什么是dom DOM(Document Object Model)是一个编程接口,它将 HTML 或 XML 文档呈现为一个由节点和对象(这些节点和对象其实是文档的各种元素、属性和文本内容等)组成的树形结构。这个树形结构允许开发者使用编程语言(如 JavaScript)来访问、修改和操作文档的内容、结构和样式。对于以下简单的 HTML 文档: <!DOCTYPE html> <html> <head> <title>My Page</title> </head> <body> <h1>Hello, World!</h1> <p id=\\\"myParagraph\\\">This is a paragraph.</p> </body> </html> 它的 DOM 树结构大致如下: - html 节点是根节点,它包含两个子节点:head 和 body。 - head 节点包含一个子节点 title,title 节点的文本内容是 \\\\\\\"My Page\\\\\\\"。 - body 节点包含两个子节点:h1 和 p。h1 节点的文本内容是 \\\\\\\"Hello, World!\\\\\\\",p 节点有一个属性 id 为 \\\\\\\"myParagraph\\\\\\\",其文本内容是 \\\\\\\"This is a paragraph.\\\\\\\"。 获取dom元素 getElementById 这是最常用的方法之一,用于通过元素的id属性获取单个元素。id在HTML文档中应该是唯一的,语法如下。 document.getElementById(\\\"elementId\\\"); 在上面的 HTML 文档中,如果要获取p元素,可以使用以下 JavaScript 代码: var paragraph = document.getElementById(\\\"myParagraph\\\"); console.log(paragraph.textContent); // 输出:This is a paragraph. getElementsByTagName 根据标签名获取元素集合。它返回一个类似数组的对象(HTMLCollection),包含所有匹配标签名的元素。语法如下: document.getElementsByTagName(\\\"tagName\\\"); 要获取文档中的所有 h1 元素: var h1Elements = document.getElementsByTagName(\\\"h1\\\"); for (var i = 0; i < h1Elements.length; i++) { console.log(h1Elements[i].textContent); // 输出:Hello, World! } getElementsByClassName 通过类名获取元素集合。同样返回一个 HTMLCollection,包含所有具有指定类名的元素。 document.getElementsByClassName(\\\"className\\\"); 假设HTML中有多个元素有highlight类,如下: <p class=\\\"highlight\\\">This is a highlighted paragraph.</p> <span class=\\\"highlight\\\">This is a highlighted span.</span> 可以使用以下代码获取这些元素: var highlightedElements = document.getElementsByClassName(\\\"highlight\\\"); for (var i = 0; i < highlightedElements.length; i++) { console.log(highlightedElements[i].textContent); // 输出两个元素的文本内容 } 修改元素 修改元素内容 textContent属性 用于获取或设置元素的文本内容,如改变前面获取的p元素的内容 var paragraph = document.getElementById(\\\"myParagraph\\\"); paragraph.textContent = \\\"This is a new paragraph content.\\\"; innerHTML属性 用于获取或设置元素内部的HTML内容。可以用于添加或修改元素内部的标签和文本。如在前面p元素内部添加一个strong标签: var paragraph = document.getElementById(\\\"myParagraph\\\"); paragraph.innerHTML = \\\"This is a <strong>modified</strong> paragraph.\\\"; 修改元素属性 获取和设置属性 可以通过元素对象的属性来获取和设置大多数 HTML 属性。例如,对于一个 元素的 src 属性。假设 HTML 中有一个img元素,可以这样修改其src属性: <img id=\\\"myImage\\\" src=\\\"original.jpg\\\"> var image = document.getElementById(\\\"myImage\\\"); image.src = \\\"new.jpg\\\"; 使用setAttribute和getAttribute setAttribute 用于设置元素的属性,getAttribute 用于获取元素的属性。如对于上面的img元素,也可以这样操作: var image = document.getElementById(\\\"myImage\\\"); image.setAttribute(\\\"src\\\", \\\"new.jpg\\\"); var currentSrc = image.getAttribute(\\\"src\\\"); console.log(currentSrc); // 输出:new.jpg 创建与添加DOM元素 创建元素 使用createElement方法。具体为先使用 document.createElement(\\\\\\\"tagName\\\\\\\") 创建一个新的 HTML元素。如创建一个新的div元素并设置其 id 和内容,如下。 var newDiv = document.createElement(\\\"div\\\"); newDiv.id = \\\"newDiv\\\"; newDiv.textContent = \\\"This is a new div.\\\"; 添加元素 appendChild 使用appendChild方法,将一个元素添加为另一个元素的子元素。例如,将新创建的div元素添加到 body元素中。 var body = document.getElementsByTagName(\\\"body\\\")[0]; var newDiv = document.createElement(\\\"div\\\"); newDiv.id = \\\"newDiv\\\"; newDiv.textContent = \\\"This is a new div.\\\"; body.appendChild(newDiv); insertBefore 在指定的子元素之前插入一个新元素。它需要两个参数:要插入的新元素和参考元素(新元素将插入在参考元素之前)。如假设有两个 p 元素在 body 中,在第一个 p 元素之前插入新的 div 元素。 <body> <p>First Paragraph</p> <p>Second Paragraph</p> </body> var body = document.getElementsByTagName(\\\"body\\\")[0]; var newDiv = document.createElement(\\\"div\\\"); newDiv.id = \\\"newDiv\\\"; newDiv.textContent = \\\"This is a new div.\\\"; var firstParagraph = document.getElementsByTagName(\\\"p\\\")[0]; body.insertBefore(newDiv, firstParagraph); 删除DOM元素 用于从父元素中删除指定的子元素,语法如下。 parentElement.removeChild(childElement); 如假设要删除前面添加的div元素(假设它有 id 为 \\\\\\\"newDiv\\\\\\\")。 var divToRemove = document.getElementById(\\\"newDiv\\\"); var body = document.getElementsByTagName(\\\"body\\\")[0]; body.removeChild(divToRemove);
  • 快速搭建一个可访问的网页

    快速搭建一个可访问的网页

    简介 搭建网站需要以下几个组件, Web服务器(如Apache、Nginx):它是网页能够被访问的核心组件。当用户在浏览器中输入网站的域名或 IP 地址并请求访问网页时,Web 服务器软件会接收这些 HTTP 请求。负责从服务器的存储设备(如硬盘)中找到对应的网页文件(如 HTML、CSS、JavaScript 文件等),并将这些文件发送回用户的浏览器。 编程语言支持(如PHP、Python、Node.js):实现网页的动态生成和业务逻辑,如果只是静态网页或不需要服务器端脚本处理PHP也不需要安装。 SSL证书(可选):用于加密HTTP通信,提升安全性。如果不需要安全链接,也不需要安装。 数据库(如MySQL、PostgreSQL)(可选):如果网页需要存储和管理大量的数据(如用户信息、文章内容、产品信息等),数据库就非常重要。 安装 安装nginx apache和nginx可以二选一 软件安装步骤 步骤1: 执行命令安装nginx sudo yum install nginx 步骤2: 启动nginx sudo systemctl start nginx 步骤3:查看启动状态 sudo systemctl status nginx 配置证书支持HTTPS 步骤1: 打开nginx.conf文件 /etc/nginx/nginx.conf 步骤2: 配置证书路径 步骤3: 重启nginx sudo systemctl restart nginx 根页面 在nginx.conf文件中,可以配置根页面,如上图的root /usr/share/nginx/html。 安装Apache 软件安装步骤 步骤1:执行命令安装Apache及其扩展包。 sudo yum -y install httpd httpd-manual httpd-devel mod_ssl mod_perl php-mysqli 如果回显信息显示Complete!,则表示Apache安装成功。 步骤2:依次执行命令启动Apache并设置自启动。 sudo systemctl start httpd sudo systemctl enable httpd 步骤3:执行命令查看Apache运行状态。如果回显信息显示active(running)时,表示Apache已启动。 systemctl status httpd 4.在本地电脑浏览器地址栏中输入http://<ECS服务器的公网IP>,测试Apache服务是否安装成功。如果显示如图所示的测试页面,表示Apache服务已安装成功。ECS服务器的弹性公网IP可以从ECS实例页面获取。 至此,按照上面流程,就可以访问一个页面了。页面的位置在;/usr/share/httpd/noindex/index.html 证书更新步骤 步骤1:找到ssl.conf文件 cd /etc/httpd/conf.d 步骤2: 编辑ssl.conf更新SSLCertificateFile、SSLCertificateKeyFile、SSLCertificateChainFile。 步骤3: 重启Apache。 sudo systemctl restart httpd 通过以上的步骤,把你的网页放到/var/www/html下面,远端就可以访问了。 数据库 暂时用不到,后续再补充。 PHP 暂时用不到,后续再补充。 其他 关于开机自启动 设置应用开机自启动 systemctl enable nginx 查询应用是否开机 systemctl list-unit-files | grep nginx 或者systemctl is-enabled nginx 关闭应用自启动 systemctl disable httpd
  • openwrt规则编译cmake软件包

    openwrt规则编译cmake软件包

    软件包中有完整CMakelists.txt include $(TOPDIR)/rules.mk PKG_NAME:=usrsctp PKG_VERSION:=0.9.5.0 PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:= https://codeload.github.com/sctplab/usrsctp/tar.gz/v$(PKG_VERSION)? PKG_HASH:=260107caf318650a57a8caa593550e39bca6943e93f970c80d6c17e59d62cd92 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE PKG_BUILD_PARALLEL:=1 include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/cmake.mk define Package/usrsctp SECTION:=libs CATEGORY:=Libraries TITLE:=sctp stack URL:=https://github.com/sctplab/usrsctp endef define Package/cJSON/description this is a userland SCTP stack endef CMAKE_OPTIONS += -DBUILD_SHARED_LIBS=on #Provide compilation dependencies for other modules to call define Build/InstallDev $(INSTALL_DIR) $(1)/usr/include $(CP) $(PKG_INSTALL_DIR)/usr/include/usrsctp.h $(1)/usr/include $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/libusrsctp.so* $(1)/usr/lib/ $(INSTALL_DIR) $(1)/usr/lib/pkgconfig $(CP) $(PKG_INSTALL_DIR)/usr/lib/pkgconfig/usrsctp.pc $(1)/usr/lib/pkgconfig $(SED) 's,/usr,$(STAGING_DIR)/usr,g' $(1)/usr/lib/pkgconfig/usrsctp.pc endef #Installed to the root file system for call at runtime define Package/usrsctp/install $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/libusrsctp.so* $(1)/usr/lib/ endef $(eval $(call BuildPackage,usrsctp)) 如上是示例,这种类型比较简单,包含了cmake.mk后,openwrt会自动生成编译,只需要安装需要被其他代码依赖的库和头文件,以及运行时需要的库即可。 软件包中有完整CMakelists.txt但需要创建build目录 include $(TOPDIR)/rules.mk PKG_NAME:=libsrtp PKG_VERSION:=2.6.0 PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/cisco/libsrtp/tar.gz/v$(PKG_VERSION)? PKG_HASH:=bf641aa654861be10570bfc137d1441283822418e9757dc71ebb69a6cf84ea6b PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE PKG_BUILD_PARALLEL:=1 include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/cmake.mk define Package/libsrtp SECTION:=libs CATEGORY:=Libraries TITLE:=libsrtp Client Library URL:=https://github.com/cisco/libsrtp endef define Package/libsrtp/description srtp Library endef CMAKE_BINARY_SUBDIR=build CMAKE_OPTIONS += -DBUILD_SHARED_LIBS=on -DTEST_APPS=off define Build/InstallDev $(INSTALL_DIR) $(1)/usr/include/srtp2 $(CP) $(PKG_INSTALL_DIR)/usr/include/srtp2/*.h $(1)/usr/include/srtp2 $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/lib*.so* $(1)/usr/lib/ endef define Package/libsrtp/install $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/lib*.so* $(1)/usr/lib/ endef $(eval $(call BuildPackage,libsrtp)) 这种相对前面的方式类似,只是这种软件包CMakelist.text要求需要创建build目录进行编译,因此在Makefile中添加CMAKE_BINARY_SUBDIR=build进行声明。因此在编译输出目录,可以看到openwrt会先创建build目录,而第一种方式是没有创建build目录。 软件包中只有部分CMakelist.text 这种方式就是软件包中没有提供完整的CMakelist.txt,但是提供了部分,需要用户包含提供部分的Cmakelist.txt实现,这种方式目前是通过打patches的方式生成主要CMakelist.txt。 Makefile文件 include $(TOPDIR)/rules.mk PKG_NAME:=coreMQTT PKG_VERSION:=2.3.1 PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:= https://codeload.github.com/FreeRTOS/coreMQTT/tar.gz/v$(PKG_VERSION)? PKG_HASH:=b8e95044e6ef8381610949b7fe546c5ddf4e52989ce9938209d5dd5f3371b5d7 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE PKG_BUILD_PARALLEL:=1 include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/cmake.mk define Package/coreMQTT SECTION:=libs CATEGORY:=Libraries TITLE:=coreMQTT Client Library URL:=https://github.com/FreeRTOS/coreMQTT endef define Package/coreMQTT/description coreMQTT Client Library endef define Build/InstallDev $(INSTALL_DIR) $(1)/usr/include $(CP) $(PKG_INSTALL_DIR)/*.h $(1)/usr/include $(CP) $(PKG_INSTALL_DIR)/include/*.h $(1)/usr/include $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/lib*.so* $(1)/usr/lib/ endef define Package/coreMQTT/install $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/libcoreMQTT.so* $(1)/usr/lib/ endef $(eval $(call BuildPackage,coreMQTT)) patches diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..754a6a2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.10) + +project(CoreMQTT) + +include(${CMAKE_CURRENT_LIST_DIR}/mqttFilePaths.cmake) + +add_definitions("-DMQTT_DO_NOT_USE_CUSTOM_CONFIG") + +include_directories ( + ${MQTT_INCLUDE_PUBLIC_DIRS} +) + +add_library(coreMQTT SHARED ${MQTT_SOURCES} ${MQTT_SERIALIZER_SOURCES}) + +install(TARGETS coreMQTT + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +install(DIRECTORY ${MQTT_INCLUDE_PUBLIC_DIRS}/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/ + FILES_MATCHING PATTERN "*.h" +) -- 2.25.1 以上是通过打patches生成主CMakelist.txt,可以看到包含了mqttFilePaths.cmake文件。
  • Cmake构建模版

    Cmake构建模版

    # 指定 CMake 的最低版本要求。它应该是 CMakeLists.txt 文件中的第一个命令。 cmake_minimum_required(VERSION 3.10) # 定义项目的名称 project(mpp_webrtc) # 要编译的源文件,先收集在SRCS变量中。 file(GLOB SRCS "*.c" "common/*.c") # 编译源文件的头文件路径 include_directories( ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/example/common /lib/xxx/a /lib/xxx/b ) # 需要链接的库路径 link_directories( /lib/A /lib/B /lib/C ) #可执行文件名称,以及依赖的源文件 add_executable(mpp_webrtc ${SRCS}) # 需要链接的库,如libwav.a, libaac.a等。其中可以使用-Wl,--start-group # 和-Wl,--end-group编译参数。 target_link_libraries(mpp_webrtc peer pthread -Wl,--start-group wav aac VE vencoder ... -Wl,--end-group )
  • 部署MQTT云服务

    部署MQTT云服务

    Centos安装 配置emqx的源 curl -s https://assets.emqx.com/scripts/install-emqx-rpm.sh | sudo bash 安装emqx sudo yum install emqx -y 启动emqx sudo systemctl start emqx 如果要卸载 sudo yum remove emqx 后台配置 访问前,服务端的端口18083权限打开。 截屏2024-12-01 08.53.43 xxx.xxx.xxx:18083 默认登录账号:admin,public。首次登录会要求更新密码。 如果要支持tcp/ssl/ws/wss这几种监听,也需要把相应的端口打开以及服务器端口权限打开。 测试访问 使用mosquitto测试 这里使用的是ubuntu系统上的mosquito_sub/pub工具测试。先安装工具: sudo apt-get install mosquitto-clients 订阅 mosquitto_sub -h www.xxx.xxx -t \"test\" -v 发布 mosquitto_pub -h www.laumy.tech -t \"test\" -m \"hello world\" 使用MQTTX软件测试 下载地址:https://mqttx.app/zh/downloads 建立一个回话连接用于订阅消息 接着对填写订阅主题 再建立一个回话用于发布消息 连接上后进行发布主题 这样订阅的回话就收到订阅消息了 开启SSL/TLS连接 参考:https://docs.emqx.com/zh/emqx/v5.8/network/overview.html 启动异常排查 查找emqx.log cd / find -name \"emqx.log*\" 参考: https://docs.emqx.com/zh/cloud/latest/connect_to_deployments/react_sdk.html https://docs.emqx.com/zh/emqx/v5.8/connect-emqx/developer-guide.html
  • coturn安装

    coturn安装

    安装 yum install coturn -y 配置 vim /etc/coturn/turnserver.conf listening-port=3478 #指定 coturn 监听的端口。默认的 TURN 协议端口是 3478,用于接收 TURN 客户端的连接请求。 tls-listening-port=5349 #指定用于 TLS (传输层安全协议) 加密连接的监听端口。TURN 支持通过 TLS 来增强通信的安全性,默认的端口是 5349。 relay-ip=172.20.65.5 #指定 TURN 服务器用于中继流量的 IP 地址。这个 IP 地址通常是内部的局域网地址,用于数据中继传输。 relay-ip=fe80::216:3eff:fe01:105b #指定 TURN 服务器的 IPv6 地址,通常用于支持 IPv6 网络的环境中。 external-ip=8.134.108.235 #指定TURN服务器的外部公网 IP 地址。当 TURN 服务器位于 NAT 后面时,external-ip 用于告知客户端如何访问该服务器。 relay-threads=50 #指定服务器处理中继流量时使用的线程数量。增加线程数有助于提高并发连接的处理能力。 min-port=40000 max-port=65535 #指定 coturn 使用的最小和最大端口范围。coturn 会在该范围内选择端口来建立临时的媒体连接。这个端口范围通常需要开放,供客户端进行通讯。 server-name=www.laumy.tech #指定 TURN 服务器的名称。这通常用于身份验证或显示给客户端的服务器标识。 lt-cred-mech # 打开这个表示要使用加密,默认是打开的,加密的话就需要验证username和密码,如下的user字段。 no-auth #如果打开这个,表示不需要加密,和lt-cred-mech是二选一。 user=laumy:A8xxxx22 #指定允许连接到 TURN 服务器的用户名和密码。格式为 username:password,用于身份验证。laumy 是用户名,A8xxxx22 是密码。 realm=www.laumy.tech #指定 TURN 服务器的“域”名,用于标识一个身份验证域。这个域通常与 user 结合使用,确保身份验证的正确性。 cert=/etc/xxx/cert/www.laumy.tech.pem pkey=/etc/nginx/cert/www.laumy.tech.key # 设置证书密钥 启停 启动: systemctl start coturn ps -ef |grep turnserver 关闭: systemctl stop coturn 调试 查看启动日志 /var/log/coturn/turnserver.log 测试 https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ 参考: https://www.zhiboblog.com/2105.html
  • STUN协议

    STUN协议

    NAT墙 完全锥形 IP限制型锥形 端口限制型锥形 对称型 NAT穿透 STUN协议简介 STUN(Session Traversal Utilities for NAT)是一个用于帮助客户端发现其所在NAT之后的公网IP地址和端口号。通信的双方都能知道所在的公网IP,并且能够让公网的NAT帮助转发,从而实现NAT穿透。STUN协议广泛应用在WebRTC实时通信系统中。STUN交互过程主要有以下几个步骤: 客户端向 STUN 服务器发送请求。 STUN 服务器处理并响应。 客户端接收响应并解析公共IP和端口。 客户端根据收到的公共 IP 和端口进行 NAT 穿越和路径选择。 绑定请求与响应 客户端发STUN请求 客户端发送STUN请求,包的类型为Binding Request。请求的目标是让STUN 服务器返回客户端的公网IP地址和端口。 STUN Message Header: - Message Type: 0x0001 (Binding Request) //类型 - Message Length: 0x0000 - Magic Cookie: 0x2112A442 //固定值,用于标识STUN - Transaction ID: 0x63f96d584f90a66d18b68202 //用于唯一标识该请求的ID,匹配响应 STUN Attributes (Optional): //以下是附加信息 - Username: <username> - Integrity: <HMAC-SHA1 signature> - Fingerprint: <CRC-32C of the message> STUN服务端响应 STUN服务器收到客户端的请求后,会放binding response回复,内容中携带了客户端的公网IP和端口号。 STUN Message Header: - Message Type: 0x0101 (Binding Response) //表明是一个 Binding Response - Message Length: 0x000C - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a66d18b68202 STUN Attributes: - Mapped Address: Family: IPv4 Address: 203.0.113.1 //公网的IP Port: 3478 //公网的端口 - Username: <username> - Integrity: <HMAC-SHA1 signature> - Fingerprint: <CRC-32C of the message> 连接检查 以webrtc举例,当需要通信的双方,通过信令交互SDP(在libpeer文章中,是通过mqtt)中交互的双方各自的公网IP地址信息后,可以使用STUN的binding request和binding response检查P2P的连通性,以确保P2P链路可达。 首先一般由一端率先发起binding request。 STUN Message Header: - Message Type: 0x0001 (Binding Request) - Message Length: 0x0008 - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a32d18b68202 STUN Attributes: - Username: <peer-username> - Message Integrity: <HMAC-SHA1 signature> 接着等待对端回复binding response STUN Message Header: - Message Type: 0x0101 (Binding Response) - Message Length: 0x0008 - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a32d18b68202 STUN Attributes: - Mapped Address: Family: IPv4 Address: 203.0.113.10 Port: 3479 当发起方接受到binding response后,表明连通性正常,下面是代码的流程示例。 agent_connectivity_check() { /*1. 发送 binding request*/ agent_create_binding_request(agent, &msg) stun_msg_create(msg, STUN_CLASS_REQUEST | STUN_METHOD_BINDING); stun_msg_write_attr(msg, STUN_ATTR_TYPE_USERNAME, strlen(username), username); stun_msg_write_attr(msg, STUN_ATTR_TYPE_PRIORITY, 4, (char*)&agent->nominated_pair->priority); stun_msg_finish(msg, STUN_CREDENTIAL_SHORT_TERM, agent->remote_upwd, strlen(agent->remote_upwd)); agent_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size);//发送,这里目标的ip和端口就是对端的ip地址和端口号。 /*2. 等待接受*/ agent_recv switch (stun_msg.stunclass) { /*收到对端发送的binding request,构建一个response发送回去。*/ case STUN_CLASS_REQUEST: agent_process_stun_request agent_create_binding_response stun_set_mapped_address agent_socket_send /*收到对端回复的binding response,将state设置为success*/ case STUN_CLASS_RESPONSE: agent_process_stun_response agent->nominated_pair->state = ICE_CANDIDATE_STATE_SUCCEEDED; } } 实践流程分析 本次实验使用的stun服务器是,有两个设备,设备A是一块开发板,设备B是电脑,分别连接到同一个路由器上。 Resolved stun.l.google.com -> 74.125.250.129, Resolved stun/turn server 74.125.250.129:19302 设备A(开发板)的IP地址信息是 inet addr:192.168.51.127 设备B(电脑)的IP地址信息是 192.168.51.227 设备发送向STUN服务端发送请求 开发板发送binding request 这里是设备向stun.l.google.com:74.125.250.129:19302服务发送binding request请求。 电脑发送binding request 设备的电脑一下发了3个binding request,其中一个是发送给google stun服务器的,但是另外两个实际上是发给设备A的本地IP和NAT墙的IP,可能是此前连接过,浏览器缓存了信息。 服务端回复 设备A收到回复 stun.l.google.com回复了binding success response。并表明了设备的公网IP为120.236.240.177,其端口为8139。 电脑B收到回复 通过message transaction ID可以看到电脑收到了Google STUN回复的binding success response,告知其NAT地址为120.236.240.177,端口号是8140。因为连接的是同一个路由器,所以NAT是一样的。同时板子A设备192.168.51.127也对齐回复了binding success response,告知其地址为192.168.51.227:59777。 板子A请求检查 板子A发送了一个binding request,对端回复了一个binding success response,则表示联通已建立。 TURN协议 TURN协议是在STUN的基础上增加了中继功能,解决的是对称型NAT无法穿透的问题。如下图的结构体,需要注意的是,使用TURN作为中继转发,传输的设备一方式TURN Client,另一方是Peer A。即TURN Client需要与TURN Server建立TURN协议交互,而Peer A与TURN Server只需要做STUN协议交互。此前调试了两个通信设备,两端都是使用TURN协议发起连接,导致一端checking失败,最后将一端修改为STUN协议交互即可。 Peer A Server-Reflexive +---------+ Transport Address | | 192.0.2.150:32102 | | | /| | TURN | / ^| Peer A | Client’s Server | / || | Host Transport Transport | // || | Address Address | // |+---------+ 10.1.1.2:49721 192.0.2.15:3478 |+-+ // Peer A | | ||N| / Host Transport | +-+ | ||A|/ Address | | | | v|T| 192.168.100.2:49582 | | | | /+-+ +---------+| | | |+---------+ / +---------+ | || |N| || | // | | | TURN |v | | v| TURN |/ | | | Client |----|A|----------| Server |------------------| Peer B | | | | |^ | |^ ^| | | | |T|| | || || | +---------+ | || +---------+| |+---------+ | || | | | || | | +-+| | | | | | | | | Client’s | Peer B Server-Reflexive Relayed Transport Transport Address Transport Address Address 192.0.2.1:7000 192.0.2.15:50000 192.0.2.210:49191 Allocate 请求服务器,分配一个中继服务。中继建立连接后,客户端需要进行保活,定期发送refresh request给服务器端。 TURN TURN Peer Peer client server A B |-- Allocate request --------------->| | | | | | | |<--------------- Allocate failure --| | | | (401 Unauthorized) | | | | | | | |-- Allocate request --------------->| | | | | | | |<---------- Allocate success resp --| | | | (192.0.2.15:50000) | | | / / / / | | | | |-- Refresh request ---------------->| | | | | | | |<----------- Refresh success resp --| | | | | | | 如上示例,客户端先发送不带验证信息的Allocate请求,此时STUN服务器会返回error response,客户端收到错误后加上验证信息再次请求。以IPC通信为例,摄像头端是Peer A(资源有限,不支持证书请求等协议),发起的是STUN协议,而手机预览端是TURN Client,发起的是TURN交互协议。 Send机制 客户端和peer直接通信可以通过TURN server进行转发信息,主要有两种方式,第一种是使用Send/data方式,第二种是使用channels方式。其主要目的是通过某种方式告知服务器,从client接收到的数据应该发给那个peer。下面是Send/data机制。 TURN Server在进行转发前,需要client先安装一个到对等端的许可(permission),许可证可以通过CreatePermission request/resp来进行交互。 TURN TURN Peer Peer client server A B | | | | |-- CreatePermission req (Peer A) -->| | | |<-- CreatePermission success resp --| | | | | | | |--- Send ind (Peer A)-------------->| | | | |=== data ===>| | | | | | | |<== data ====| | |<-------------- Data ind (Peer A) --| | | | | | | | | | | |--- Send ind (Peer B)-------------->| | | | | dropped | | | | | | | |<== data ==================| | dropped | | | | | | | Chanel机制 等同于send/data机制。 TURN TURN Peer Peer client server A B | | | | |-- ChannelBind req ---------------->| | | | (Peer A to 0x4001) | | | | | | | |<---------- ChannelBind succ resp --| | | | | | | |-- [0x4001] data ------------------>| | | | |=== data ===>| | | | | | | |<== data ====| | |<------------------ [0x4001] data --| | | | | | | |--- Send ind (Peer A)-------------->| | | | |=== data ===>| | | | | | | |<== data ====| | |<------------------ [0x4001] data --| | | | | | | 参考: https://mp.weixin.qq.com/s/XMhSDABc74dpALIHrPPt7w https://www.ctyun.cn/developer/article/586260405821509 https://www.cnblogs.com/pannengzhi/p/5048965.html
  • libpeer分析

    libpeer分析

    关键数据结构 PeerConfiguration typedef struct PeerConfiguration { IceServer ice_servers[5]; MediaCodec audio_codec; MediaCodec video_codec; DataChannelType datachannel; void (*onaudiotrack)(uint8_t* data, size_t size, void* userdata); void (*onvideotrack)(uint8_t* data, size_t size, void* userdata); void (*on_request_keyframe)(void* userdata); void* user_data; } PeerConfiguration; 实例: PeerConfiguration config = { .ice_servers = { {.urls = "stun:stun.l.google.com:19302"}, }, .datachannel = DATA_CHANNEL_STRING, .video_codec = CODEC_H264, .audio_codec = CODEC_PCMA}; Agent Agent 结构体,主要用于实现 WebRTC 或类似协议中的 ICE (Interactive Connectivity Establishment) 机制。ICE 是一种 NAT 穿越协议,用于在点对点通信中建立连接并选择最佳的网络路径。Agent 结构体存储了与 ICE 协议操作相关的各种数据,用于管理连接的候选地址、协商过程以及状态。 struct Agent { char remote_ufrag[ICE_UFRAG_LENGTH + 1]; char remote_upwd[ICE_UPWD_LENGTH + 1]; char local_ufrag[ICE_UFRAG_LENGTH + 1]; char local_upwd[ICE_UPWD_LENGTH + 1]; IceCandidate local_candidates[AGENT_MAX_CANDIDATES]; IceCandidate remote_candidates[AGENT_MAX_CANDIDATES]; int local_candidates_count; int remote_candidates_count; UdpSocket udp_sockets[2]; Address host_addr; int b_host_addr; uint64_t binding_request_time; AgentState state; AgentMode mode; IceCandidatePair candidate_pairs[AGENT_MAX_CANDIDATE_PAIRS]; IceCandidatePair* selected_pair; IceCandidatePair* nominated_pair; int candidate_pairs_num; int use_candidate; uint32_t transaction_id[3]; }; ServiceConfiguration typedef struct ServiceConfiguration { const char* mqtt_url; int mqtt_port; const char* client_id; const char* http_url; int http_port; const char* username; const char* password; PeerConnection* pc; } ServiceConfiguration; 实例 #define SERVICE_CONFIG_DEFAULT() \ { \ .mqtt_url = "broker.emqx.io", \ //访问的MQTT服务器 .mqtt_port = 8883, \ //访问的MQTT端口 .client_id = "peer", \ //用于订阅的主题 webrtc/peer/jsonrpc //发布的主题 webrtc/peer/jsonrpc-reply .http_url = "", \ //信令使用的是MQTT,没有使用http, .http_port = 443, \ //因此HTTP的没有用。 .username = "", \ .password = "", \ .pc = NULL \ } PeerConnection struct PeerConnection { PeerConfiguration config; PeerConnectionState state; Agent agent; DtlsSrtp dtls_srtp; Sctp sctp; Sdp local_sdp; Sdp remote_sdp; void (*onicecandidate)(char* sdp, void* user_data); void (*oniceconnectionstatechange)(PeerConnectionState state, void* user_data); void (*on_connected)(void* userdata); void (*on_receiver_packet_loss)(float fraction_loss, uint32_t total_loss, void* user_data); uint8_t temp_buf[CONFIG_MTU]; uint8_t agent_buf[CONFIG_MTU]; int agent_ret; int b_local_description_created; Buffer* audio_rb; Buffer* video_rb; Buffer* data_rb; RtpEncoder artp_encoder; RtpEncoder vrtp_encoder; RtpDecoder vrtp_decoder; RtpDecoder artp_decoder; uint32_t remote_assrc; uint32_t remote_vssrc; }; WebRTC简介 概览 上图中一共有4中角色,分别是signaling server、STUN/RURN server、Client A,Client B; signaling server:信令指的是管理两个通信设备A和B建立和管理点对点连接过程中的控制消息交换机制。让通信双方能够交换各种信息,从而建立、维护和终止一个点对点的实时通信连接。 STUN/RURN server:用于设备A和设备B传过NAT。 Client A:通信设备A Client B:通信设备B 通信流程 WebRTC中客户端与信令服务器、STUN/TURN服务器的交互流程如下: ClientA首先创建PeerConnection对象,然后打开本地音视频设备,将音视频数据封装成MediaStream添加到PeerConnection中。 ClientA调用PeerConnection的CreateOffer方法创建一个用于offer的SDP对象,SDP对象中保存当前音视频的相关参数。ClientA通过PeerConnection的SetLocalDescription方法将该SDP对象保存起来,并通过Signal服务器发送给ClientB。 ClientB接收到ClientA发送过的offer SDP对象,通过PeerConnection的SetRemoteDescription方法将其保存起来,并调用PeerConnection的CreateAnswer方法创建一个应答的SDP对象,通过PeerConnection的SetLocalDescription的方法保存该应答SDP对象并将它通过Signal服务器发送给ClientA。 ClientA接收到ClientB发送过来的应答SDP对象,将其通过PeerConnection的SetRemoteDescription方法保存起来。 在SDP信息的offer/answer流程中,ClientA和ClientB已经根据SDP信息创建好相应的音频Channel和视频Channel并开启Candidate数据的收集,Candidate数据可以简单地理解成Client端的IP地址信息(本地IP地址、公网IP地址、Relay服务端分配的地址)。 当ClientA收集到Candidate信息后,PeerConnection会通过OnIceCandidate接口给ClientA发送通知,ClientA将收到的Candidate信息通过Signal服务器发送给ClientB,ClientB通过PeerConnection的AddIceCandidate方法保存起来。同样的操作ClientB对ClientA再来一次。 这样ClientA和ClientB就已经建立了音视频传输的P2P通道,ClientB接收到ClientA传送过来的音视频流,会通过PeerConnection的OnAddStream回调接口返回一个标识ClientA端音视频流的MediaStream对象,在ClientB端渲染出来即可。同样操作也适应ClientB到ClientA的音视频流的传输。 上述序列中,WebRTC并不提供Stun服务器和Signal服务器,服务器端需要自己实现。Stun服务器可以用google提供的实现stun协议的测试服务器(stun:stun.l.google.com:19302),Signal服务器则完全需要自己实现了,可以使用MQTT的broker.emqx.io来作为信令服务器。它需要在ClientA和ClientB之间传送彼此的SDP信息和candidate信息,ClientA和ClientB通过这些信息建立P2P连接来传送音视频数据。 信令 待补充 SDP SDP是一个比较老的协议,发布于2006年,以type=value的格式描述回话内容。WebRTC引入SDP来描述媒体信息,用于媒体协商时决定双方是否可以进行通信。当对端设备anser的时候会携带一个SDP格式的内容,如下。 v=0 o=- 8646007345366799659 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE video audio datachannel a=msid-semantic: WMS 5a162afd-e195-4207-a699-e977bb510327 m=video 10773 UDP/TLS/RTP/SAVPF 96 102 c=IN IP4 14.29.67.186 a=rtcp:9 IN IP4 0.0.0.0 a=candidate:1969734079 1 udp 2122260223 192.168.31.123 65492 typ host generation 0 network-id 1 network-cost 10 a=candidate:2345473323 1 tcp 1518280447 192.168.31.123 9 typ host tcptype active generation 0 network-id 1 network-cost 10 a=candidate:3819939686 1 udp 1686052607 14.29.67.186 10773 typ srflx raddr 192.168.31.123 rport 65492 generation 0 network-id 1 network-cost 10 a=ice-ufrag:JgBs a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 a=setup:active a=mid:video a=recvonly a=rtcp-mux a=rtpmap:96 H264/90000 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=rtpmap:102 H264/90000 a=rtcp-fb:102 nack a=rtcp-fb:102 nack pli a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f m=audio 9 UDP/TLS/RTP/SAVP 8 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:JgBs //对端用户名 a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v //对端的密码 a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 //指纹信息 a=setup:active a=mid:audio a=sendrecv a=msid:5a162afd-e195-4207-a699-e977bb510327 693effe8-ba8d-469d-ba6a-f58e2b5f73df a=rtcp-mux a=rtpmap:8 PCMA/8000 a=ssrc:1668486723 cname:o+PtcUckwEIRvBPp m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=ice-ufrag:JgBs a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 a=setup:active a=mid:datachannel a=sctp-port:5000 a=max-message-size:262144 ICE candidates ICE (Interactive Connectivity Establishment 互动连接建立) candidates(候选人)简称ICE candidates,WebRTC中的ICE Candidate是用来描述可以连接的远端的基本信息,什么是candidate,两台设备的连接,需要知道设备的网络信息,如IP地址,端口号以及使用的协议,因此candidate至少包含{address,port,protocol}三元组信息集。通过SDP会话来交互candidate信息。 WebRTC将Candidate分成四种类型,且类型间存在优先级次序,从高到低分别为host、srflx、prflx和relay,本章节使用的是srflx类型,从STUN服务器获取的地址。 host:从本机网卡上获取到的地址,一般来说,一个网卡对应一个地址。 srflx(server reflexive):从STUN服务器获取到的地址。 relay:从TRUN服务器获取到的地址。 prflx(peer reflexive):在交互过程中从对端数据报文中获取到的地址。 其中,srflx和prflx地址可能是一样的,但获取的途径不一样,下面是描述ICE candidates的数据结构。 typedef enum IceCandidateType { ICE_CANDIDATE_TYPE_HOST, ICE_CANDIDATE_TYPE_SRFLX, ICE_CANDIDATE_TYPE_PRFLX, ICE_CANDIDATE_TYPE_RELAY, } IceCandidateType; typedef struct IceCandidate IceCandidate; struct IceCandidate { int foundation; int component; //1:RTP,2:RTCP uint32_t priority;//优先级 char transport[32 + 1]; //传输协议,基于UDP IceCandidateType type; //类型,如上。 IceCandidateState state; Address addr; Address raddr; }; main函数分析 /*1. 调用srtp_init,初始化srtp内核模块加密套件、debug等*/ peer_init(); /*2. 创建一个peer连接,其中创建了agent,打开了一个udp socket,初始化 了rtp 音频、视频编解码,主要是确定用什么编码格式。*/ g_pc = peer_connection_create(&config); /*3. 注册一个peer连接的状态切换回调函数,回调函数 onconnectionstatechange, 用于处理对等连接的 ICE 连接状态变更。当连接的状态发生变化(例如从new到connected, 或者 failed),这个回调函数将被调用。*/ peer_connection_oniceconnectionstatechange(g_pc, onconnectionstatechange); /*4. 设置数据通道(DataChannel)相关的回调函数。当数据通道打开时,会触发 onopen;接收到消息时,会触发 onmessage;当数据通道关闭时会触发onclose*/ peer_connection_ondatachannel(g_pc, onmessage, onopen, onclose); /*5. 设置信令服务器的配置,通常用于 WebRTC 会话中的信令部分。通过传入 service_config 配置,WebRTC 客户端可以向信令服务器注册或传递配置信息 (如 client_id 和对等连接句柄 pc) 这里的信令服务器用的是公用的mqtt服务器? broker.emqx.io 使用mqtt来作为信令通信,没有使用http,在里面会注册一个peer_connect的候选回调 */ /*什么是信令 在webrtc中,信令指的是建立和管理点对点连接过程中的控制消息交换机制。让通信双方 能够交换各种信息,从而建立、维护和终止一个点对点的实时通信连接。 WebRTC 的核心目标是实现浏览器之间的直接通信,支持音频、视频和数据传输, 但在建立这种连接之前,双方需要交换一些控制信息,包括: - 会话描述(如 SDP,Session Description Protocol):描述连接的媒体设置(如视频编码格式、音频参数等)。 - 网络候选(ICE候选,ICE candidates):用于寻找最佳的点对点网络路径,以确保即使在复杂的网络环境下也能建立连接。Interactive Connectivity Establishment(互动连接建立) - 连接状态:例如连接是否已建立、是否关闭等。 */ /*信令的实现 虽然 WebRTC 协议本身并没有指定信令的实现方式,但是它提供了用于交换数据的 API。 开发者可以选择适合自己的信令协议和传输方式,通常的实现方式包括: - WebSocket:一个常见的双向通信协议,可以实时地交换信令数据。 - HTTP 请求:通过轮询、长轮询等方式进行信令交换。 - SIP (Session Initiation Protocol):一些应用可能采用 SIP 作为信令协议。 - XMPP (Extensible Messaging and Presence Protocol):也是一种可以用来实现信令的协议,尤其适用于即时消息和通信应用。 - MQTT:通过订阅和发布的方式来进行传输,本文就是使用这种方式。 */ /*信令的工作流程 假设有两个用户 A 和 B 通过 WebRTC 建立视频通话,信令的过程大致如下: (1) 用户A生成offer:用 A创建一个 RTCPeerConnection 对象,并 生成一个offer。这个 offer 包含了 A 的媒体设置和网络候选信息(ICE 候选)。 A 将这个 offer 通过信令系统(例如 MQTT)发送给用户 B。 (2)用户B接收到 offer,并生成 answer:用户 B 收到 offer 后,使用它来创 建一个 RTCPeerConnection,并生成一个 answer(应答)。这个 answer 包含 B 的媒体配置和 ICE 候选信息。B 将 answer 通过信令系统发送给用户 A。 (3)交换 ICE 候选:在连接过程中,双方都会收集到 ICE 候选并通过信令进行交换, 直到双方都确定了最佳的网络路径。 (4)建立连接:当双方完成了 ICE 候选交换并且建立了连接时,通信就可以开始了。 (5)连接终止:当会话结束时,双方通过信令通知对方关闭连接。*/ service_config.client_id = argv[1]; service_config.pc = g_pc; peer_signaling_set_config(&service_config); /*6.初始化MQTT,连接broker.emqx.io, */ peer_signaling_join_channel(); /*7. 创建peer连接的task*/ pthread_create(&peer_connection_thread, NULL, peer_connection_task, NULL); /*8. 创建signal的task*/ pthread_create(&peer_singaling_thread, NULL, peer_singaling_task, NULL); /*9. 读取本地音视频初始化*/ reader_init(); /*10. 循环判断PEER的状态是否是连接态,如果是已经连接,则发送音视频数据*/ while (!g_interrupted) { if (g_state == PEER_CONNECTION_COMPLETED) { curr_time = get_timestamp(); // FPS 25 if (curr_time - video_time > 40) { video_time = curr_time; if (reader_get_video_frame(buf, &size) == 0) { peer_connection_send_video(g_pc, buf, size); } } if (curr_time - audio_time > 20) { if (reader_get_audio_frame(buf, &size) == 0) { peer_connection_send_audio(g_pc, buf, size); } audio_time = curr_time; } usleep(1000); } } 信令交互 int peer_signaling_join_channel() { /*1. 与信令服务器建立连接,实际上就是连接MQTT的broker*/ if (peer_signaling_mqtt_connect(g_ps.mqtt_host, g_ps.mqtt_port) < 0) { LOGW("Connect MQTT server failed"); return -1; } /*1. 发布信令的消息*/ peer_signaling_mqtt_subscribe(1); return 0; } 与信令服务器建立连接 因为与MQTT broker.emqx.io的交互是TLS加解密的,因此先使用ssl_transport_connect建立加解密通道,接着对MQTT进行初始化,将ssl的收发函数注册到MQTT中,这样MQTT后续的就可以用SSL的加解密通道进行通信,最后是发起MQTT连接。 static int peer_signaling_mqtt_connect(const char* hostname, int port) { MQTTStatus_t status; MQTTConnectInfo_t conn_info; bool session_present; /*1. 与mqtt的broker,其host="broker.emqx.io",port=8883建立TLS连接,MQTT的收发消息 是否通过加密的方式,因此需要先使用TLS进行连接*/ if (ssl_transport_connect(&g_ps.net_ctx, hostname, port, NULL) < 0) { LOGE("ssl transport connect failed"); return -1; } /*2. ssl建立连接之后,将发送和接收的函数通过MQTT_Init进行注册,后续其收发就会调 调用ssl_transport_recv和ssl_transport_send进行解加密收发。 MQTT_Init初始化时还注册回调函数, 对CONNACK/PUBLISH/SUBACK事件进处理 需要注意的时,当设备收到broker发过来的PUBLISH事件时,会回调这个函数进行 处理,这样的场景是当对端设备访问连接是,就会调用peer_signaling_mqtt_event_cb。*/ g_ps.transport.recv = ssl_transport_recv; g_ps.transport.send = ssl_transport_send; g_ps.transport.pNetworkContext = &g_ps.net_ctx; g_ps.mqtt_fixed_buf.pBuffer = g_ps.mqtt_buf; g_ps.mqtt_fixed_buf.size = sizeof(g_ps.mqtt_buf); status = MQTT_Init(&g_ps.mqtt_ctx, &g_ps.transport, ports_get_epoch_time, peer_signaling_mqtt_event_cb, &g_ps.mqtt_fixed_buf); memset(&conn_info, 0, sizeof(conn_info)); conn_info.cleanSession = false; if (strlen(g_ps.username) > 0) { conn_info.pUserName = g_ps.username; conn_info.userNameLength = strlen(g_ps.username); } if (strlen(g_ps.password) > 0) { conn_info.pPassword = g_ps.password; conn_info.passwordLength = strlen(g_ps.password); } if (strlen(g_ps.client_id) > 0) { conn_info.pClientIdentifier = g_ps.client_id; conn_info.clientIdentifierLength = strlen(g_ps.client_id); } conn_info.keepAliveSeconds = KEEP_ALIVE_TIMEOUT_SECONDS; /*3.建立MQTT的,MQTT协议与broker连接。前面步骤1是建立SSL连接,相当于是建立了加解密通道, 而这里是建立MQTT的协议通道。*/ status = MQTT_Connect(&g_ps.mqtt_ctx, &conn_info, NULL, CONNACK_RECV_TIMEOUT_MS, &session_present); if (status != MQTTSuccess) { LOGE("MQTT_Connect failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } LOGI("MQTT_Connect succeeded."); return 0; } 通知信令服务器已准备 通知服务器已经准备,主要是通过MQTT的订阅主题为\"webrtc/peer/jsonrpc\",等待接入设备接入。 static int peer_signaling_mqtt_subscribe(int subscribed) { MQTTStatus_t status = MQTTSuccess; MQTTSubscribeInfo_t sub_info; /*1. 获取MQTT的packet id*/ uint16_t packet_id = MQTT_GetPacketId(&g_ps.mqtt_ctx);、 /*2. MQTT QOS为0即只发一次,订阅的主题是"webrtc/peer/jsonrpc" */ memset(&sub_info, 0, sizeof(sub_info)); sub_info.qos = MQTTQoS0; sub_info.pTopicFilter = g_ps.subtopic; sub_info.topicFilterLength = strlen(g_ps.subtopic); /*3. 发送订阅消息*/ if (subscribed) { status = MQTT_Subscribe(&g_ps.mqtt_ctx, &sub_info, 1, packet_id); } else { status = MQTT_Unsubscribe(&g_ps.mqtt_ctx, &sub_info, 1, packet_id); } if (status != MQTTSuccess) { LOGE("MQTT_Subscribe failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } /*4. 等待MQTT 订阅消息发送成功*/ status = MQTT_ProcessLoop(&g_ps.mqtt_ctx); /* 调用该函数实时处理循环接收数据,并自定发送心跳包保持链接,当有数据到来时,会触发myEventCallback回调函数*/ if (status != MQTTSuccess) { LOGE("MQTT_ProcessLoop failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } LOGD("MQTT Subscribe/Unsubscribe succeeded."); return 0; } 接收信令服务器事件处理 在peer_signaling_mqtt_connect函数中,调用MQTT_Init初始化时,注册了一个peer_signaling_mqtt_event_cb回调函数,其对接收的MQTT消息进行处理,包括收到了Broker发过来的PUBLISH消息,因此当设备接入时,调用的是peer_signaling_mqtt_event_cb函数。 static void peer_signaling_mqtt_event_cb(MQTTContext_t* mqtt_ctx, MQTTPacketInfo_t* packet_info, MQTTDeserializedInfo_t* deserialized_info) { switch (packet_info->type) { case MQTT_PACKET_TYPE_CONNACK: LOGI("MQTT_PACKET_TYPE_CONNACK"); break; case MQTT_PACKET_TYPE_PUBLISH: LOGI("MQTT_PACKET_TYPE_PUBLISH"); /*收到了broker发送过来的PUBLISH消息*/ peer_signaling_on_pub_event(deserialized_info->pPublishInfo->pPayload, deserialized_info->pPublishInfo->payloadLength); break; case MQTT_PACKET_TYPE_SUBACK: LOGD("MQTT_PACKET_TYPE_SUBACK"); break; default: break; } } static void peer_signaling_on_pub_event(const char* msg, size_t size) { cJSON *req, *res, *item, *result, *error; int id = -1; char* payload = NULL; PeerConnectionState state; req = res = item = result = error = NULL; /*1. 先获取peer connect的状态*/ state = peer_connection_get_state(g_ps.pc); printf("%s,%d, msg:%s\n",__func__,__LINE__,msg); do { /*2. 解析json文件*/ req = cJSON_Parse(msg); if (!req) { error = cJSON_CreateRaw(RPC_ERROR_PARSE_ERROR); LOGW("Parse json failed"); break; } /*3. 解析jsion中字段的id值*/ item = cJSON_GetObjectItem(req, "id"); if (!item && !cJSON_IsNumber(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_REQUEST); LOGW("Cannot find id"); break; } id = item->valueint; /*4. 解析jison中的method字段是是什么?*/ item = cJSON_GetObjectItem(req, "method"); if (!item && cJSON_IsString(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_REQUEST); LOGW("Cannot find method"); break; } /* 5. 解析json文件,发现metho为offer,broker发过来要求offer,表示有设备加入了。*/ /* {"jsonrpc":"2.0","method":"offer","id":89} */ if (strcmp(item->valuestring, RPC_METHOD_OFFER) == 0) { switch (state) { case PEER_CONNECTION_NEW: case PEER_CONNECTION_DISCONNECTED: case PEER_CONNECTION_FAILED: case PEER_CONNECTION_CLOSED: { g_ps.id = id; /*6. 将peer connect状态切换为PEER_CONNECTION_NEW 在peer_connection_loop中就会调用到peer_connection_state_new, offer一个SDP*/ peer_connection_create_offer(g_ps.pc); } break; default: { error = cJSON_CreateRaw(RPC_ERROR_INTERNAL_ERROR); } break; } /*7. broker发送过来的是anwer,表示对端设备的应答*/ } else if (strcmp(item->valuestring, RPC_METHOD_ANSWER) == 0) { item = cJSON_GetObjectItem(req, "params"); if (!item && !cJSON_IsString(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_PARAMS); LOGW("Cannot find params"); break; } /*8. 收到对端应答后,解析对端anser的SDP内容,设置远端的描述信息*/ if (state == PEER_CONNECTION_NEW) { peer_connection_set_remote_description(g_ps.pc, item->valuestring); result = cJSON_CreateString(""); } } else if (strcmp(item->valuestring, RPC_METHOD_STATE) == 0) { result = cJSON_CreateString(peer_connection_state_to_string(state)); } else if (strcmp(item->valuestring, RPC_METHOD_CLOSE) == 0) { peer_connection_close(g_ps.pc); result = cJSON_CreateString(""); } else { error = cJSON_CreateRaw(RPC_ERROR_METHOD_NOT_FOUND); LOGW("Unsupport method"); } } while (0); /*9. 发布消息,发布什么内容??*/ if (result || error) { res = cJSON_CreateObject(); cJSON_AddStringToObject(res, "jsonrpc", RPC_VERSION); cJSON_AddNumberToObject(res, "id", id); if (result) { cJSON_AddItemToObject(res, "result", result); } else if (error) { cJSON_AddItemToObject(res, "error", error); } payload = cJSON_PrintUnformatted(res); if (payload) { peer_signaling_mqtt_publish(&g_ps.mqtt_ctx, payload); free(payload); } cJSON_Delete(res); } if (req) { cJSON_Delete(req); } } Offer SDP 当对端Client B设备通过信令服务器发起连接时,信令服务器的MQTT broker发送publish给当前Client A设备,接收到MQTT 的publish后,解析到method为offer,即将peer的状态切换为PEER_CONNECTION_NEW,继而peer_connection_loop中状态切换为peer_connection_state_new。 static void peer_connection_state_new(PeerConnection* pc, DtlsSrtpRole role, int isOfferer) { char* description = (char*)pc->temp_buf; memset(pc->temp_buf, 0, sizeof(pc->temp_buf)); dtls_srtp_reset_session(&pc->dtls_srtp); /*dtls srtp 初始化,在后续的章节再进行描述。*/ dtls_srtp_init(&pc->dtls_srtp, role, pc); pc->dtls_srtp.udp_recv = peer_connection_dtls_srtp_recv; pc->dtls_srtp.udp_send = peer_connection_dtls_srtp_send; pc->sctp.connected = 0; if (isOfferer) { agent_clear_candidates(&pc->agent); pc->agent.mode = AGENT_MODE_CONTROLLING; } else { pc->agent.mode = AGENT_MODE_CONTROLLED; } /*1. 通过stun传输获取candidate信息*/ agent_gather_candidate(&pc->agent, NULL, NULL, NULL); // host address for (int i = 0; i < sizeof(pc->config.ice_servers) / sizeof(pc->config.ice_servers[0]); ++i) { if (pc->config.ice_servers[i].urls) { LOGI("ice server: %s", pc->config.ice_servers[i].urls); agent_gather_candidate(&pc->agent, pc->config.ice_servers[i].urls, pc->config.ice_servers[i].username, pc->config.ice_servers[i].credential); } } /*将candidate信息转化为SDP格式?*/ agent_get_local_description(&pc->agent, description, sizeof(pc->temp_buf)); memset(&pc->local_sdp, 0, sizeof(pc->local_sdp)); // TODO: check if we have video or audio codecs /*创建一个SDP会话*/ sdp_create(&pc->local_sdp, pc->config.video_codec != CODEC_NONE, pc->config.audio_codec != CODEC_NONE, pc->config.datachannel); /*填充其他的sdp信息*/ if (pc->config.video_codec == CODEC_H264) { sdp_append_h264(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); } switch (pc->config.audio_codec) { case CODEC_PCMA: sdp_append_pcma(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); break; case CODEC_PCMU: sdp_append_pcmu(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); break; case CODEC_OPUS: sdp_append_opus(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); default: break; } if (pc->config.datachannel) { sdp_append_datachannel(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); } pc->b_local_description_created = 1; /*将SDP信息发送出去。*/ if (pc->onicecandidate) { pc->onicecandidate(pc->local_sdp.content, pc->config.user_data); } } anser SDP peer_signaling_on_pub_event strcmp(item->valuestring, RPC_METHOD_ANSWER peer_connection_set_remote_description(g_ps.pc, item->valuestring); 设备收到publish消息后,解析jsion判断出method是anser,调用peer_connection_set_remote_description进行处理。 void peer_connection_set_remote_description(PeerConnection* pc, const char* sdp_text) { char* start = (char*)sdp_text; char* line = NULL; char buf[256]; char* val_start = NULL; uint32_t* ssrc = NULL; DtlsSrtpRole role = DTLS_SRTP_ROLE_SERVER; int is_update = 0; Agent* agent = &pc->agent; /*1. 解析SDP会话内容*/ while ((line = strstr(start, "\n"))) { line = strstr(start, "\n"); strncpy(buf, start, line - start); buf[line - start] = '\0'; if (strstr(buf, "a=setup:passive")) { role = DTLS_SRTP_ROLE_CLIENT; } if (strstr(buf, "a=fingerprint")) { strncpy(pc->dtls_srtp.remote_fingerprint, buf + 22, DTLS_SRTP_FINGERPRINT_LENGTH); } if (strstr(buf, "a=ice-ufrag") && strlen(agent->remote_ufrag) != 0 && (strncmp(buf + strlen("a=ice-ufrag:"), agent->remote_ufrag, strlen(agent->remote_ufrag)) == 0)) { is_update = 1; } if (strstr(buf, "m=video")) { ssrc = &pc->remote_vssrc; } else if (strstr(buf, "m=audio")) { ssrc = &pc->remote_assrc; } if ((val_start = strstr(buf, "a=ssrc:")) && ssrc) { *ssrc = strtoul(val_start + 7, NULL, 10); LOGD("SSRC: %" PRIu32, *ssrc); } start = line + 2; } if (is_update) { return; } if (!pc->b_local_description_created) { peer_connection_state_new(pc, role, 0); } /*2. 设置远程的描述信息,主要是填充PeerConnection中agent的信息, 这个agent信息存储了本地和远端的icecandicate*/ agent_set_remote_description(&pc->agent, (char*)sdp_text); /*3. 将peer connect状态切换为checking*/ STATE_CHANGED(pc, PEER_CONNECTION_CHECKING); } 测试P2P通路 收到对端的anser SDP后,解析得到了ICE candidate,那么将peer connect切换为checking状态,先测试一下P2P的通路。 int agent_connectivity_check(Agent* agent) { char addr_string[ADDRSTRLEN]; uint8_t buf[1400]; StunMessage msg; if (agent->nominated_pair->state != ICE_CANDIDATE_STATE_INPROGRESS) { LOGI("nominated pair is not in progress"); return -1; } memset(&msg, 0, sizeof(msg)); /*1. 使用对端的ip地址发送数据*/ if (agent->nominated_pair->conncheck % AGENT_CONNCHECK_PERIOD == 0) { addr_to_string(&agent->nominated_pair->remote->addr, addr_string, sizeof(addr_string)); LOGD("send binding request to remote ip: %s, port: %d", addr_string, agent->nominated_pair->remote->addr.port); agent_create_binding_request(agent, &msg); agent_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size); } /*2. 接收对端的反馈数据,如果返回正确,表示链路连通正常*/ agent_recv(agent, buf, sizeof(buf)); if (agent->nominated_pair->state == ICE_CANDIDATE_STATE_SUCCEEDED) { agent->selected_pair = agent->nominated_pair; return 0; } return -1; } 测试P2P链路正常后,就将Peer Connection状态切换为PEER_CONNECTION_CONNECTED,接下来就是正式的P2P链路创建连接了。 P2P通信 通过上面信令的建立,P2P链路已经建立了连接,接下来就是创建P2P的通信链路了,P2P的传输使用的是SRTP链路。 初始化 在offer SDP的时候,函数peer_connection_state_new中会调用dtls_srtp_init进行初始化。 int dtls_srtp_init(DtlsSrtp* dtls_srtp, DtlsSrtpRole role, void* user_data) { static const mbedtls_ssl_srtp_profile default_profiles[] = { MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80, MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32, MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_80, MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_32, MBEDTLS_TLS_SRTP_UNSET}; /*1. 设置srtp的角色,目前是做server,并更新为INIT状态,并设置UDP收发的回调函数。*/ dtls_srtp->role = role; dtls_srtp->state = DTLS_SRTP_STATE_INIT; dtls_srtp->user_data = user_data; dtls_srtp->udp_send = dtls_srtp_udp_send; dtls_srtp->udp_recv = dtls_srtp_udp_recv; /*2.初始化 MBEDTLS 相关结构体*/ mbedtls_ssl_config_init(&dtls_srtp->conf); //初始化 ssl_config 结构体,这是 SSL 配置的核心,包含加密协议、证书、密钥等信息 mbedtls_ssl_init(&dtls_srtp->ssl); //初始化 ssl 结构体,表示一个 SSL 会话上下文。 mbedtls_x509_crt_init(&dtls_srtp->cert); //初始化 X.509 证书结构,用于存储公钥证书。 mbedtls_pk_init(&dtls_srtp->pkey); //初始化公钥对象结构,用于存储私钥。 mbedtls_entropy_init(&dtls_srtp->entropy); //初始化熵源,熵源是生成随机数所需的基础。 mbedtls_ctr_drbg_init(&dtls_srtp->ctr_drbg); //初始化 CTR_DRBG(Counter-based Deterministic Random Byte Generator), //它是用于生成随机数的伪随机数生成器。 /*3. 设置打印等级*/ #if CONFIG_MBEDTLS_DEBUG mbedtls_debug_set_threshold(3); mbedtls_ssl_conf_dbg(&dtls_srtp->conf, dtls_srtp_debug, NULL); #endif /*4.生成一个自签名证书。这个证书将在 DTLS 握手期间用于身份验证*/ dtls_srtp_selfsign_cert(dtls_srtp); /*5. 配置证书验证和认证模式*/ mbedtls_ssl_conf_verify(&dtls_srtp->conf, dtls_srtp_cert_verify, NULL); //配置证书验证回调函数 dtls_srtp_cert_verify,用来验证远端证书的有效性。 mbedtls_ssl_conf_authmode(&dtls_srtp->conf, MBEDTLS_SSL_VERIFY_REQUIRED); //设置身份验证模式为MBEDTLS_SSL_VERIFY_REQUIRED,即客户端必须提供有效证书进行验证(服务器验证客户端的证书)。 /*6. 配置证书链和私钥*/ mbedtls_ssl_conf_ca_chain(&dtls_srtp->conf, &dtls_srtp->cert, NULL); //配置服务器的根证书链,这里使用dtls_srtp->cert 作为证书链的起始证书。 mbedtls_ssl_conf_own_cert(&dtls_srtp->conf, &dtls_srtp->cert, &dtls_srtp->pkey); //配置本地证书和私钥。服务器和客户端都需要自己的证书和私钥来进行加密通信。 /*7. 配置随机数生成器和超时*/ mbedtls_ssl_conf_rng(&dtls_srtp->conf, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); //配置随机数生成器,使用 mbedtls_ctr_drbg_random 和 dtls_srtp->ctr_drbg 作为随机数源。 mbedtls_ssl_conf_read_timeout(&dtls_srtp->conf, 1000); //设置读取超时时间为 1000 毫秒,意味着在等待数据时,如果超过 1000 毫秒没有接收到数据,则超时。 /*8. 如果是服务器 (DTLS_SRTP_ROLE_SERVER),则配置为 DTLS 服务器模式,并设置用 于防止 DoS 攻击的 DTLS cookies(cookie 是用于防止重放攻击的)。 如果角色是客户端,则直接配置为 DTLS 客户端模式。这里是做服务端*/ if (dtls_srtp->role == DTLS_SRTP_ROLE_SERVER) { mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); mbedtls_ssl_cookie_init(&dtls_srtp->cookie_ctx); mbedtls_ssl_cookie_setup(&dtls_srtp->cookie_ctx, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); mbedtls_ssl_conf_dtls_cookies(&dtls_srtp->conf, mbedtls_ssl_cookie_write, mbedtls_ssl_cookie_check, &dtls_srtp->cookie_ctx); } else { mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); } /*10. 生成并打印本地指纹, 用于生成本地证书的指纹(一个哈希值),用于标识证书。*/ dtls_srtp_x509_digest(&dtls_srtp->cert, dtls_srtp->local_fingerprint); LOGD("local fingerprint: %s", dtls_srtp->local_fingerprint); /*11. 配置 DTLS-SRTP 的保护配置文件,default_profiles 是预定义的安全配置, 包含 SRTP 协议所支持的加密算法、密钥长度等信息。*/ mbedtls_ssl_conf_dtls_srtp_protection_profiles(&dtls_srtp->conf, default_profiles); /*12. 配置 SRTP MKI(主密钥标识符)的支持情况。在这里,设置为不支持 MKI。*/ mbedtls_ssl_conf_srtp_mki_value_supported(&dtls_srtp->conf, MBEDTLS_SSL_DTLS_SRTP_MKI_UNSUPPORTED); /*13. 禁用客户端证书请求中的 CA 列表。*/ mbedtls_ssl_conf_cert_req_ca_list(&dtls_srtp->conf, MBEDTLS_SSL_CERT_REQ_CA_LIST_DISABLED); /*14. 完成所有配置后,调用 mbedtls_ssl_setup 初始化 ssl 上下文并将配置应用于 当前的 DTLS 会话。*/ mbedtls_ssl_setup(&dtls_srtp->ssl, &dtls_srtp->conf); return 0; } 创建 case PEER_CONNECTION_CONNECTED: /*1. 建立DTLS SRTP链接*/ if (dtls_srtp_handshake(&pc->dtls_srtp, NULL) == 0) { LOGD("DTLS-SRTP handshake done"); /*2. 建立sctp 链接*/ if (pc->config.datachannel) { LOGI("SCTP create socket"); sctp_create_socket(&pc->sctp, &pc->dtls_srtp); pc->sctp.userdata = pc->config.user_data; } STATE_CHANGED(pc, PEER_CONNECTION_COMPLETED); } 主要是创建DTLS_SRTP和SCTP两条链路。 DTSL_SRTP dtls_srtp_handshake_server() { /* 循环发起*/ while(1) { mbedtls_ssl_session_reset(&dtls_srtp->ssl); mbedtls_ssl_set_client_transport_id(&dtls_srtp->ssl, client_ip, sizeof(client_ip)); /* 发起握手*/ ret = dtls_srtp_do_handshake(dtls_srtp); if (ret == MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED) { LOGD("DTLS hello verification requested"); } else if (ret != 0) { LOGE("failed! mbedtls_ssl_handshake returned -0x%.4x", (unsigned int)-ret); break; } else { break; } } } 判断返回的错误是MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED,会进行再次循环发起。 static int dtls_srtp_do_handshake(DtlsSrtp* dtls_srtp) { int ret; /* timer 是 mbedtls_timing_delay_context 类型的一个静态变量,用于管理 定时器(通常是控制超时的机制)。这个定时器在 DTLS 协议中用于处理超时等操作。*/ static mbedtls_timing_delay_context timer; /*定时器是 DTLS 握手中用来处理超时的一个重要机制,因为 DTLS 是基于UDP协议的, 而 UDP 本身不保证数据的可靠传输,因此需要在握手过程中手动管理超时。*/ mbedtls_ssl_set_timer_cb(&dtls_srtp->ssl, &timer, mbedtls_timing_set_delay, mbedtls_timing_get_delay); #if CONFIG_MBEDTLS_2_X /*回调函数会在 DTLS 握手完成后用于派发加密密钥。*/ mbedtls_ssl_conf_export_keys_ext_cb(&dtls_srtp->conf, dtls_srtp_key_derivation_cb, dtls_srtp); #else mbedtls_ssl_set_export_keys_cb(&dtls_srtp->ssl, dtls_srtp_key_derivation_cb, dtls_srtp); #endif /*用于设置 DTLS 上下文的 BIO(Basic Input/Output)回调函数,这些回调函数 定义了数据的发送和接收方式。回调函数在dtls_srtp_init进行初始化的。*/ mbedtls_ssl_set_bio(&dtls_srtp->ssl, dtls_srtp, dtls_srtp->udp_send, dtls_srtp->udp_recv, NULL); /*执行 DTLS 握手,直到握手成功或发生错误。*/ do { ret = mbedtls_ssl_handshake(&dtls_srtp->ssl); } while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); return ret; } SSL的握手 /* Main handshake loop */ while (ssl->state != MBEDTLS_SSL_HANDSHAKE_OVER) { ret = mbedtls_ssl_handshake_step(ssl); if (ret != 0) { break; } } 主要就是调用mbedtls_ssl_handshake_step,下面只列出了服务端的代码示例。 int mbedtls_ssl_handshake_step(mbedtls_ssl_context *ssl) { int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED; ret = ssl_prepare_handshake_step(ssl); if (ret != 0) { return ret; } ret = mbedtls_ssl_handle_pending_alert(ssl); if (ret != 0) { goto cleanup; } #if defined(MBEDTLS_SSL_SRV_C) if (ssl->conf->endpoint == MBEDTLS_SSL_IS_SERVER) { /*调用mbedtls_ssl_handshake_server_step进行握手管理*/ if (mbedtls_ssl_conf_is_tls12_only(ssl->conf)) { ret = mbedtls_ssl_handshake_server_step(ssl); } } #endif if (ret != 0) { /* handshake_step return error. And it is same * with alert_reason. */ if (ssl->send_alert) { ret = mbedtls_ssl_handle_pending_alert(ssl); goto cleanup; } } cleanup: return ret; } 主要是接着调用mbedtls_ssl_handshake_server_step进行处理。 int mbedtls_ssl_handshake_server_step(mbedtls_ssl_context *ssl) { int ret = 0; MBEDTLS_SSL_DEBUG_MSG(2, ("server state: %d", ssl->state)); switch (ssl->state) { case MBEDTLS_SSL_HELLO_REQUEST: ssl->state = MBEDTLS_SSL_CLIENT_HELLO; break; /* * <== ClientHello */ case MBEDTLS_SSL_CLIENT_HELLO: ret = ssl_parse_client_hello(ssl); break; #if defined(MBEDTLS_SSL_PROTO_DTLS) case MBEDTLS_SSL_SERVER_HELLO_VERIFY_REQUEST_SENT: return MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED; #endif /* * ==> ServerHello * Certificate * ( ServerKeyExchange ) * ( CertificateRequest ) * ServerHelloDone */ case MBEDTLS_SSL_SERVER_HELLO: ret = ssl_write_server_hello(ssl); break; case MBEDTLS_SSL_SERVER_CERTIFICATE: ret = mbedtls_ssl_write_certificate(ssl); break; case MBEDTLS_SSL_SERVER_KEY_EXCHANGE: ret = ssl_write_server_key_exchange(ssl); break; case MBEDTLS_SSL_CERTIFICATE_REQUEST: ret = ssl_write_certificate_request(ssl); break; case MBEDTLS_SSL_SERVER_HELLO_DONE: ret = ssl_write_server_hello_done(ssl); /* * <== ( Certificate/Alert ) * ClientKeyExchange * ( CertificateVerify ) * ChangeCipherSpec * Finished */ case MBEDTLS_SSL_CLIENT_CERTIFICATE: ret = mbedtls_ssl_parse_certificate(ssl); break; case MBEDTLS_SSL_CLIENT_KEY_EXCHANGE: ret = ssl_parse_client_key_exchange(ssl); break; case MBEDTLS_SSL_CERTIFICATE_VERIFY: ret = ssl_parse_certificate_verify(ssl); break; case MBEDTLS_SSL_CLIENT_CHANGE_CIPHER_SPEC: ret = mbedtls_ssl_parse_change_cipher_spec(ssl); break; case MBEDTLS_SSL_CLIENT_FINISHED: ret = mbedtls_ssl_parse_finished(ssl); break; /* * ==> ( NewSessionTicket ) * ChangeCipherSpec * Finished */ case MBEDTLS_SSL_SERVER_CHANGE_CIPHER_SPEC: #if defined(MBEDTLS_SSL_SESSION_TICKETS) if (ssl->handshake->new_session_ticket != 0) { ret = ssl_write_new_session_ticket(ssl); } else #endif ret = mbedtls_ssl_write_change_cipher_spec(ssl); break; case MBEDTLS_SSL_SERVER_FINISHED: ret = mbedtls_ssl_write_finished(ssl); break; case MBEDTLS_SSL_FLUSH_BUFFERS: MBEDTLS_SSL_DEBUG_MSG(2, ("handshake: done")); ssl->state = MBEDTLS_SSL_HANDSHAKE_WRAPUP; break; case MBEDTLS_SSL_HANDSHAKE_WRAPUP: mbedtls_ssl_handshake_wrapup(ssl); break; default: MBEDTLS_SSL_DEBUG_MSG(1, ("invalid state %d", ssl->state)); return MBEDTLS_ERR_SSL_BAD_INPUT_DATA; } return ret; } 主要就是针对握手的状态进行握手交互,具体这里就不阐述了,可以参考《SSL/TLS协议分析》文章。 SCTP SCTP主要使用的usrctp开源组件, 传输 while (!g_interrupted) { if (g_state == PEER_CONNECTION_COMPLETED) { curr_time = get_timestamp(); // FPS 25 if (curr_time - video_time > 40) { video_time = curr_time; if (reader_get_video_frame(buf, &size) == 0) { peer_connection_send_video(g_pc, buf, size); } } if (curr_time - audio_time > 20) { if (reader_get_audio_frame(buf, &size) == 0) { peer_connection_send_audio(g_pc, buf, size); } audio_time = curr_time; } usleep(1000); } }
  • 乌龟图分析法

    乌龟图分析法

    结构形似乌龟,身体是主体,头尾脚代表着互相关联的六个要素。 身体:代表过程、活动。 头部:达标input,即过程的要求,是分析的关键。 左前脚:代表Procedure,指的操作的方法,怎么做? 右前脚:达标Resources,是做成这件事需要的非人工资源。 右后脚:代表who,谁负责这个事情,对人有什么要求? 左后脚:代表KPI,衡量这个过程的指标,有效性。 尾部:代表Output,即过程产生的结果,输出。 乌龟图主要用在任务的落地和安排上。
  • 看wireshark报文技巧

    看wireshark报文技巧

    TCP握手与挥手 握手看SYNC与ACK 第一次握手[SYNC]标志, 第二次握手看[SYNC,ACK]标志,第三次ACK。 挥手看FIN 报文交互 正常交互 先看len,当长度为0,这个包是ack包,长度不为0表示是发送的数据包。 再看ack,表示该值以前的序号都收到了。ack是对端上一个报文的seq+len,同时也是期望对端下一个发包。 警告交互 TCP Dup ACK 重传ACK[TCP dup ack AAA#B],其中A表示重复发送的是哪个number的报文(注意不是seq),B表示这是第几次重复发送ACK。主要有以下场景会导致。 回复的ACK丢了导致对端没有收到,导致对端重复发送报文,以至于期望接收的报文没有收到,需要重复发送ACK。 序列468的重发实际是多余,接收者在466序列已经收到了。 TCP Out-of-Order 报文乱序,发送报文的seq不对,一般情况下是对TCP Dup ACK的重传。
\t