深入理解rtmp(二)之C++脚手架搭建

前面深入理解rtmp(1)之开发环境搭建中我们已经搭建好服务器,并且利用一些现成的工具可以推送直播流,播放直播流了.这篇文章我们开始搭建从零开发一套rtmp推流拉流sdk,对着协议实现,达到真正的"深入理解".

作为一个码农,搬砖搬到一定高度就需要"脚手架"来支撑我们"够得住".为了方面我们把rtmp推拉流sdk实现为一个PC上的命令行程序,当开发调试稳定后,我们可以快速的通过交叉编译工具编译到Android/iOS等移动端设备.

1.创建工程

我们使用cmake作为安装编译工具,需要安装cmake,mac下执行brew install cmake.
在我们的rtmpsdk路径下创建CMakeLists.txt:

//指定cmake最低版本
cmake_minimum_required (VERSION 3.6)set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}" CACHE PATH "Installation directory" FORCE)
message(STATUS "CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}")set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fPIC -ffunction-sections -fdata-sections -Os")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC -ffunction-sections -fdata-sections -Os")project (rtmpsdk)set(SRC_PREFIX "src")set(SELF_LIBS_OUT ${CMAKE_SYSTEM_NAME}.out)file(GLOB SELF_SRC_FILES ${SRC_PREFIX}/main.cpp)add_executable(${PROJECT_NAME} ${SELF_SRC_FILES})

创建src目录,创建main.cpp文件:

#include <iostream>
int main(int argc,char* argv[])
{//标准输出到控制台std::cout << "Hello rtmp server!" << std::endl;return 0;    
}

在rtmpsdk下创建cmake_build文件夹作为我们的输出路径
在控制台,我们进入我们的工程路径后执行:

cd cmake_build

然后执行:

cmake ..
make 

在camke下面生成了编译中间文件和最终的rtmpsdk文件:
在这里插入图片描述

现在执行一下./rtmpsdk:

$ ./rtmpsdk 
Hello rtmp server!

可以看到我们打印的"Hello rtmp server!",编译环境已经搭建好了,可以继续往下实现我们的功能了.

注:我的开发环境是mac,windows环境后面我提供一个docker的centos镜像作为我们工程的编译环境.

2.封装接口

我们想象一下,我们的rtmp应该对外提供什么接口?封装什么数据结构?

  1. 我们要连接我们的服务器,rtmp是基于tcp,那么我们要创建一个socket网络套接字,那么我们需要一个根据url创建对象的接口rtmp_t rtmp_create(const char* url)
  2. 创建socket后我们还需要做一些配置,最基本的我们要配置读写超时时间,如果我们的socket没有超时,我们的读写函数一直没有返回,会导致无法退出的问题,所以我们需要提供一个设置读写超时的接口:int rtmp_set_timeout(rtmp_t rtmp, int recv_timeout_ms, int send_timeout_ms)
  3. rtmp有握手过程,接下来需要一个握手接口:int rtmp_handshake(rtmp_t rtmp)
  4. 握手成功后开始连接服务器,提供连接接口:int rtmp_connect_app(rtmp_t rtmp)
  5. 连接成功后通知服务器是拉流还是推流,提供两个函数:int rtmp_play_stream(rtmp_t rtmp),int rtmp_publish_stream(rtmp_t rtmp)
  6. 可以开始拉流或推流了:int rtmp_read_packet(rtmp_t rtmp, char* type, uint32_t* timestamp, char** data, int* size),int rtmp_write_packet(rtmp_t rtmp, char type, uint32_t timestamp, char* data, int size)
  7. 拉推流结束后,销毁对象释放资源:void rtmp_destroy(rtmp_t rtmp)

以播放为例用一个图表示:
在这里插入图片描述

接口定义好了,我们在src下新建libs目录,创建我们对外暴露的rtmpsdk.hpp文件:

#ifndef LIB_RTMP_HPP
#define LIB_RTMP_HPP/*** rtmpsdk is a librtmp like library,* used to play/publish rtmp stream from/to rtmp server.* socket: use sync and block socket to connect/recv/send data with server.* depends: no need other libraries; depends on ssl if use complex_handshake.* thread-safe: no*/#ifndef __STDC_FORMAT_MACROS#define __STDC_FORMAT_MACROS
#endif#include <stdint.h>
#include <sys/types.h>#ifdef __cplusplus
extern "C"{
#endif/**************************************************************************************************************************** RTMP protocol context***************************************************************************************************************************/
// the RTMP handler.
typedef void* rtmp_t;/*** Create a RTMP handler.* @param url The RTMP url, for example, rtmp://localhost/live/livestream* @remark default timeout to 30s if not set by rtmp_set_timeout.* @remark default schema to url_schema_normal, use rtmp_set_schema to change it.** @return a rtmp handler, or NULL if error occured.*/
extern rtmp_t rtmp_create(const char* url);
/*** set socket timeout* @param recv_timeout_ms the timeout for receiving messages in ms.* @param send_timeout_ms the timeout for sending message in ms.* @remark user can set timeout once rtmp_create,*      or before rtmp_handshake or rtmp_dns_resolve to connect to server.* @remark default timeout to 30s if not set by rtmp_set_timeout.** @return 0, success; otherswise, failed.*/
extern int rtmp_set_timeout(rtmp_t rtmp, int recv_timeout_ms, int send_timeout_ms);
/*** close and destroy the rtmp stack.* @remark, user should never use the rtmp again.*/
extern void rtmp_destroy(rtmp_t rtmp);/**************************************************************************************************************************** RTMP protocol stack***************************************************************************************************************************/
/*** connect and handshake with server* category: publish/play* previous: rtmp-create* next: connect-app** @return 0, success; otherswise, failed.*/
/*** simple handshake specifies in rtmp 1.0,* not depends on ssl.*/
/***      rtmp_handshake equals to invoke:*       rtmp_dns_resolve()*       rtmp_connect_server()*       rtmp_do_simple_handshake()* user can use these functions if needed.*/
extern int rtmp_handshake(rtmp_t rtmp);/*** Connect to RTMP tcUrl(Vhost/App), similar to flash AS3 NetConnection.connect(tcUrl).* @remark When connected to server, user can retrieve informations from RTMP handler,*      for example, use rtmp_get_server_id to get server ip/pid/cid.* @return 0, success; otherswise, failed.*/
extern int rtmp_connect_app(rtmp_t rtmp);/*** play a live/vod stream.* category: play* previous: connect-app* next: destroy* @return 0, success; otherwise, failed.*/
extern int rtmp_play_stream(rtmp_t rtmp);/*** publish a live stream.* category: publish* previous: connect-app* next: destroy* @return 0, success; otherwise, failed.*/
extern int rtmp_publish_stream(rtmp_t rtmp);/*** E.4.1 FLV Tag, page 75*/
// 8 = audio
#define RTMP_TYPE_AUDIO 8
// 9 = video
#define RTMP_TYPE_VIDEO 9
// 18 = script data
#define RTMP_TYPE_SCRIPT 18
/*** read a audio/video/script-data packet from rtmp stream.* @param type, output the packet type, macros:*            RTMP_TYPE_AUDIO, FlvTagAudio*            RTMP_TYPE_VIDEO, FlvTagVideo*            RTMP_TYPE_SCRIPT, FlvTagScript*            otherswise, invalid type.* @param timestamp, in ms, overflow in 50days* @param data, the packet data, according to type:*             FlvTagAudio, @see "E.4.2.1 AUDIODATA"*            FlvTagVideo, @see "E.4.3.1 VIDEODATA"*            FlvTagScript, @see "E.4.4.1 SCRIPTDATA"*            User can free the packet by rtmp_free_packet.* @param size, size of packet.* @return the error code. 0 for success; otherwise, error.** @remark: for read, user must free the data.* @remark: for write, user should never free the data, even if error.** @return 0, success; otherswise, failed.*/
extern int rtmp_read_packet(rtmp_t rtmp, char* type, uint32_t* timestamp, char** data, int* size);
// @param data User should never free it anymore.
extern int rtmp_write_packet(rtmp_t rtmp, char type, uint32_t timestamp, char* data, int size);#ifdef __cplusplus
}
#endif#endif

接口定义好后,我们开始按步骤实现接口,下面我们开始实现第一步rtmp_create,通过url创建socket.

3.封装网络接口

封装网络接口前,我们先对linux c网络编程做一个回顾

3.1linux c socket编程基本流程

我们先来一张图:
在这里插入图片描述

我们的rtmpsdk作为 tcp客户端,我们再一起了解一下linux c关于socket的api

3.1.1 socket()
函数原型
int socket(int domain, int type, int protocol);
参数说明
  • domain: 协议域,又称协议族(family)。常用的协议族有 AF_INET 、 AF_INET6 、 AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE 等。协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址(32位的)与端口号(16位的)的组合、AF_UNIX 决定了要用一个绝对路径名作为地址。
  • type: 指定 Socket 类型。常用的 socket 类型有 SOCK_STREAM 、 SOCK_DGRAM 、 SOCK_RAW 、 SOCK_PACKET 、 SOCK_SEQPACKET 等。流式 Socket(SOCK_STREAM)是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用。数据报式 Socket(SOCK_DGRAM)是一种无连接的 Socket,对应于无连接的 UDP 服务应用。
  • protocol: 指定协议。常用协议有 IPPROTO_TCP 、 IPPROTO_UDP 、 IPPROTO_STCP 、 IPPROTO_TIPC 等,分别对应 TCP 传输协议、UDP 传输协议、STCP 传输协议、TIPC 传输协议。

注意:1.type 和 protocol 不可以随意组合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。

返回值

如果调用成功就返回新创建的套接字的描述符,如果失败就返回 INVALID_SOCKET(Linux下失败返回-1)。

套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。

3.1.2 bind()

bind()函数把一个地址族中的特定地址赋给socket。例如对应 AF_INET、AF_INET6 就是把一个 ipv4 或 ipv6 地址和端口号组合赋给socket。

函数原型
int bind(int socketfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
  • socketfd: 一个标识已连接套接口的描述字。
  • address: 是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。
  • address_len: 确定 address 缓冲区的长度。

其中,sockaddr 这个地址结构根据地址创建 socket 时的地址协议族的不同而不同。

如ipv4对应的是:

struct sockaddr_in {sa_family_t    sin_family; /* address family: AF_INET */in_port_t      sin_port;   /* port in network byte order */struct in_addr sin_addr;   /* internet address */
};
/* Internet address. */
struct in_addr {uint32_t       s_addr;     /* address in network byte order */
};

ipv6对应的是:

struct sockaddr_in6 { sa_family_t     sin6_family;   /* AF_INET6 */ in_port_t       sin6_port;     /* port number */ uint32_t        sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr;     /* IPv6 address */ uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};struct in6_addr { unsigned char   s6_addr[16];   /* IPv6 address */ 
};

Unix域对应的是:

#define UNIX_PATH_MAX    108
struct sockaddr_un { sa_family_t sun_family;               /* AF_UNIX */ char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};
返回值

如果函数执行成功,返回值为0,否则为SOCKET_ERROR。

3.1.3listen()

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

函数原型
int listen(int socketfd, int backlog);
参数说明
  • socketfd: 要监听的socket的描述字。
  • backlog: 相应socket可以排队的最大连接个数。

socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

3.1.4connect()
函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
  • socketfd: 客户端socket的描述字。
  • sockaddr: 服务器的socket地址。
  • addrlen: socket地址的长度
3.1.5. accept()

TCP服务器端依次调用 socket()、bind()、listen() 之后,就会监听指定的 socket 地址了。TCP客户端依次调用 socket()、connect() 之后就向 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept() 函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

函数原型
int accept(int socketfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
参数说明
  • socketfd: 就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
  • sockaddr: 结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
  • len: 它也是结果的参数,用来接受上述 addr 的结构的大小的,它指明 addr 结构所占有的字节个数。同样的,它也可以被设置为NULL。

如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。

accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。

  • 监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)
  • 连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。

一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号

3.1.6. read()、write()等

当服务器与客户端已经建立好连接,可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
函数原型1
int recv(SOCKET socket, char FAR* buf, int len, int flags);
参数说明1
  • socket: 一个标识已连接套接口的描述字。
  • buf: 用于接收数据的缓冲区。
  • len: 缓冲区长度。
  • flags: 指定调用方式。取值:MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;MSG_OOB 处理带外数据。
    若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
函数原型2
ssize_t recvfrom(int sockfd, void buf, int len, unsigned int flags, struct socketaddr* from, socket_t* fromlen);
参数说明2
  • sockfd: 标识一个已连接套接口的描述字。
  • buf: 接收数据缓冲区。
  • len: 缓冲区长度。
  • flags: 调用操作方式。是以下一个或者多个标志的组合体,可通过or操作连在一起:
  • MSG_DONTWAIT:操作不会被阻塞;
  • MSG_ERRQUEUE: 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。
  • MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
  • MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对packet套接字有效。
  • MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
  • MSG_EOR:指示记录的结束,返回的数据完成一个记录。
  • MSG_TRUNC:指明数据报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
  • MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。(MSG_TRUNC使用错误,4才是MSG_TRUNC的正确解释)
  • MSG_OOB:指示接收到out-of-band数据(即需要优先处理的数据)。
  • MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。
  • from:(可选)指针,指向装有源地址的缓冲区。
  • fromlen:(可选)指针,指向from缓冲区长度值。
函数原型3
int sendto( SOCKET s, const char FAR* buf, int size, int flags, const struct sockaddr FAR* to, int tolen);
参数说明3
  • s: 套接字
  • buf: 待发送数据的缓冲区
  • size: 缓冲区长度
  • flags: 调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
  • addr: (可选)指针,指向目的套接字的地址
  • tolen: addr所指地址的长度
    如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。
函数原型4
int accept( int fd, struct socketaddr* addr, socklen_t* len);
参数说明4
  • fd: 套接字描述符。
  • addr: 返回连接着的地址
  • len: 接收返回地址的缓冲区长度
    成功返回客户端的文件描述符,失败返回-1。
3.1.7. close()

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字。

函数原型
int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

3.2封装socket

我们把socket和超时配置等封装到一个结构体:

struct BlockSyncSocket
{SOCKET fd;int    family;int64_t rbytes;int64_t sbytes;// The send/recv timeout in ms.int64_t rtm;int64_t stm;BlockSyncSocket() {stm = rtm = UTIME_NO_TIMEOUT;rbytes = sbytes = 0;SOCKET_RESET(fd);SOCKET_SETUP();}virtual ~BlockSyncSocket() {SOCKET_CLOSE(fd);SOCKET_CLEANUP();}
};

通过上面分析知,我们需要设计socket创建,连接,读写,设置超时等:

/*** simple socket stream,* use tcp socket, sync block mode*/
class SimpleSocketStream
{
private:BlockSyncSocket* io;
public:SimpleSocketStream();virtual ~SimpleSocketStream();
public:virtual BlockSyncSocket* hijack_io();virtual int create_socket(std::string url);virtual int connect(const char* server, int port);public:virtual error_t read(void* buf, size_t size, ssize_t* nread);public:virtual void set_recv_timeout(utime_t tm);virtual utime_t get_recv_timeout();virtual int64_t get_recv_bytes();
public:virtual void set_send_timeout(utime_t tm);virtual utime_t get_send_timeout();virtual int64_t get_send_bytes();virtual error_t writev(const iovec *iov, int iov_size, ssize_t* nwrite);
public:virtual error_t read_fully(void* buf, size_t size, ssize_t* nread);virtual error_t write(void* buf, size_t size, ssize_t* nwrite);
};

接下来我们实现网络封装接口:

#include <netinet/tcp.h>#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/uio.h>#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <netdb.h>#include <bs_socket.hpp>BlockSyncSocket* hijack_io_create()
{BlockSyncSocket* skt = new BlockSyncSocket();return skt;
}
void hijack_io_destroy(BlockSyncSocket* ctx)
{freep(ctx);
}
int hijack_io_create_socket(BlockSyncSocket* skt,std::string url)
{skt->family = AF_INET6;skt->fd = ::socket(skt->family, SOCK_STREAM, 0);   // Try IPv6 first.if (!SOCKET_VALID(skt->fd)) {skt->family = AF_INET;skt->fd = ::socket(skt->family, SOCK_STREAM, 0);   // Try IPv4 instead, if IPv6 fails.}if (!SOCKET_VALID(skt->fd)) {return ERROR_SOCKET_CREATE;}// No TCP cache.int v = 1;setsockopt(skt->fd, IPPROTO_TCP, TCP_NODELAY, &v, sizeof(v));return ERROR_SUCCESS;
}
int hijack_io_connect(BlockSyncSocket* skt, const char* server_ip, int port)
{char sport[8];snprintf(sport, sizeof(sport), "%d", port);addrinfo hints;memset(&hints, 0, sizeof(hints));hints.ai_family   = skt->family;hints.ai_socktype = SOCK_STREAM;addrinfo* r  = NULL;AutoFree(addrinfo, r);if(getaddrinfo(server_ip, sport, (const addrinfo*)&hints, &r)) {return ERROR_SOCKET_CONNECT;}if(::connect(skt->fd, r->ai_addr, r->ai_addrlen) < 0){return ERROR_SOCKET_CONNECT;}return ERROR_SUCCESS;
}
int hijack_io_read(BlockSyncSocket* skt, void* buf, size_t size, ssize_t* nread)
{int ret = ERROR_SUCCESS;ssize_t nb_read = ::recv(skt->fd, (char*)buf, size, 0);if (nread) {*nread = nb_read;}// On success a non-negative integer indicating the number of bytes actually read is returned// (a value of 0 means the network connection is closed or end of file is reached).if (nb_read <= 0) {if (nb_read < 0 && SOCKET_ERRNO() == SOCKET_ETIME) {return ERROR_SOCKET_TIMEOUT;}if (nb_read == 0) {errno = SOCKET_ECONNRESET;}return ERROR_SOCKET_READ;}skt->rbytes += nb_read;return ret;
}
int hijack_io_set_recv_timeout(BlockSyncSocket* skt, int64_t tm)
{// The default for this option is zero,// which indicates that a receive operation shall not time out.int32_t sec = 0;int32_t usec = 0;if (tm != UTIME_NO_TIMEOUT) {sec = (int32_t)(tm / 1000);usec = (int32_t)((tm % 1000)*1000);}struct timeval tv = { sec , usec };if (setsockopt(skt->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) {return SOCKET_ERRNO();}skt->rtm = tm;return ERROR_SUCCESS;
}int hijack_io_set_send_timeout(BlockSyncSocket* skt, int64_t tm)
{// The default for this option is zero,// which indicates that a receive operation shall not time out.int32_t sec = 0;int32_t usec = 0;if (tm != UTIME_NO_TIMEOUT) {sec = (int32_t)(tm / 1000);usec = (int32_t)((tm % 1000)*1000);}struct timeval tv = { sec , usec };if (setsockopt(skt->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) == -1) {return SOCKET_ERRNO();}skt->stm = tm;return ERROR_SUCCESS;
}int hijack_io_writev(BlockSyncSocket* skt, const iovec *iov, int iov_size, ssize_t* nwrite)
{int ret = ERROR_SUCCESS;ssize_t nb_write = ::writev(skt->fd, iov, iov_size);if (nwrite) {*nwrite = nb_write;}// On  success,  the  readv()  function  returns the number of bytes read;// the writev() function returns the number of bytes written.  On error, -1 is// returned, and errno is set appropriately.if (nb_write <= 0) {if (nb_write < 0 && SOCKET_ERRNO() == SOCKET_ETIME) {return ERROR_SOCKET_TIMEOUT;}return ERROR_SOCKET_WRITE;}skt->sbytes += nb_write;return ret;
}int hijack_io_read_fully(BlockSyncSocket* skt, void* buf, size_t size, ssize_t* nread)
{int ret = ERROR_SUCCESS;size_t left = size;ssize_t nb_read = 0;while (left > 0) {char* this_buf = (char*)buf + nb_read;ssize_t this_nread;if ((ret = hijack_io_read(skt, this_buf, left, &this_nread)) != ERROR_SUCCESS) {return ret;}nb_read += this_nread;left -= (size_t)this_nread;}if (nread) {*nread = nb_read;}skt->rbytes += nb_read;return ret;
}
int hijack_io_write(BlockSyncSocket* skt, void* buf, size_t size, ssize_t* nwrite)
{int ret = ERROR_SUCCESS;ssize_t nb_write = ::send(skt->fd, (char*)buf, size, 0);if (nwrite) {*nwrite = nb_write;}if (nb_write <= 0) {if (nb_write < 0 && SOCKET_ERRNO() == SOCKET_ETIME) {return ERROR_SOCKET_TIMEOUT;}return ERROR_SOCKET_WRITE;}skt->sbytes += nb_write;return ret;
}error_t SimpleSocketStream::read(void* buf, size_t size, ssize_t* nread)
{assert(io);int ret = hijack_io_read(io, buf, size, nread);if (ret != ERROR_SUCCESS) {return error_new(ret, "read");}return success;
}

接下来我们就可以在我们的main函数里面创建SimpleSocketStream,然后创建socket了.下一篇我们开始通过创建的socket进行rtmp握手.

3.3测试

在我们的main.cpp中:

#include <iostream>
#include <bs_socket.hpp>
int main(int argc,char* argv[])
{std::cout << "Hello rtmp server!" << std::endl;SimpleSocketStream *sss = new SimpleSocketStream();if(sss->create_socket("rtmp://127.0.0.1:1935/live/livestream") != 0){printf("create socket error!");return -1;}std::cout<< "create fd = " << sss->hijack_io()->fd << std::endl;free(sss);return 0;    
}

输出结果:

$ ./rtmpsdk 
Hello rtmp server!
create fd = 3

我们成功创建了句柄为3的socket.

题外话

linux网络编程中有同步/异步,阻塞/非阻塞,由于我们现在sdk是客户端,没有并发连接的问题,所以我们的实现使用阻塞同步socket.
我们在创建socket时兼容了ipv6,先尝试ipv6,如果失败了再尝试ipv4:

int hijack_io_create_socket(BlockSyncSocket* skt,std::string url)
{skt->family = AF_INET6;skt->fd = ::socket(skt->family, SOCK_STREAM, 0);   // Try IPv6 first.if (!SOCKET_VALID(skt->fd)) {skt->family = AF_INET;skt->fd = ::socket(skt->family, SOCK_STREAM, 0);   // Try IPv4 instead, if IPv6 fails.}if (!SOCKET_VALID(skt->fd)) {return ERROR_SOCKET_CREATE;}// No TCP cache.int v = 1;setsockopt(skt->fd, IPPROTO_TCP, TCP_NODELAY, &v, sizeof(v));return ERROR_SUCCESS;
}

setsockopt可以对socket进行设置,这里:IPPROTO_TCP 和 IPPROTO_IP代表两种不同的协议,分别代表IP协议族里面的TCP协议和IP协议
TCP_NODELAY是什么呢?
TCP/IP协议中针对TCP默认开启了Nagle算法。Nagle算法通过减少需要传输的数据包,来优化网络。在内核实现中,数据包的发送和接受会先做缓存,分别对应于写缓存和读缓存。

启动TCP_NODELAY,就意味着禁用了Nagle算法,允许小包的发送。对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择。rtmp是直播流式传输,对延时比较敏感,所以我们关闭了NODELAY.同时比如,对于SSH会话,用户在远程敲击键盘发出指令的速度相对于网络带宽能力来说,绝对不是在一个量级上的,所以数据传输非常少;而又要求用户的输入能够及时获得返回,有较低的延时。如果开启了Nagle算法,就很可能出现频繁的延时,导致用户体验极差。当然,你也可以选择在应用层进行buffer,比如使用java中的buffered stream,尽可能地将大包写入到内核的写缓存进行发送;vectored I/O(writev接口)也是个不错的选择。

对于关闭TCP_NODELAY,则是应用了Nagle算法。数据只有在写缓存中累积到一定量之后,才会被发送出去,这样明显提高了网络利用率(实际传输数据payload与协议头的比例大大提高)。但是这又不可避免地增加了延时;与TCP delayed ack这个特性结合,这个问题会更加显著,延时基本在40ms左右。当然这个问题只有在连续进行两次写操作的时候,才会暴露出来。

连续进行多次对小数据包的写操作,然后进行读操作,本身就不是一个好的网络编程模式;在应用层就应该进行优化。
对于既要求低延时,又有大量小数据传输,还同时想提高网络利用率的应用,大概只能用UDP自己在应用层来实现可靠性保证了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1452454.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

主观评测图生3D之 --- Era3D

文章目录 概述真人测试&#xff08;一般&#xff09;动物&#xff08;猫猫狗狗&#xff0c;不ok&#xff09;Q版真人&#xff08;惊艳&#xff09; 概述 抱抱脸可以直接测试 不过抱抱脸只能够生成多视图图像以及对应的法向图。 评测的话&#xff0c;拿三类我们比较关心的图片…

SSM医院线上线下全诊疗系统-计算机毕业设计源码02210

目 录 摘要 1 绪论 1.1背景及意义 1.2研究现状 1.3ssm框架介绍 1.4论文结构与章节安排 2 医院线上线下全诊疗系统系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分…

基于RandLA-Net深度学习模型的激光点云语义分割

一、场景要素语义分割部分的文献阅读笔记 RandLA-Net是一种高效、轻量级的神经网络&#xff0c;其可直接逐点推理大规模点云的语义标签。RandLA-Net基于随机点采样获得了显著的计算和内存效率&#xff0c;并采用新的局部特征聚合模块有效地保留了几何细节&#xff0c;弥补了随机…

【机器学习】机器学习中的人工神经元模型有哪些?

线性神经元 线性神经元&#xff08;Linear Neuron&#xff09;是一种基本的人工神经元模型&#xff0c;特点是其输出是输入的线性组合。线性神经元是神经网络中最简单的一种形式&#xff0c;适用于处理线性关系的问题。数学模型如下&#xff0c; y w ⋅ x b ∑ i 1 n w i x…

CC2500和CC1101移植说明

主要通过如何移植、移植注意、关于芯片配置、如何生成导出配置四大步骤来说明CC2500和CC1101移植 首先通过下图1这个宏进行选择 如何移植 要移植的部分在 CC2500_hal.c 和 CC2500_hal.h中, 搜索 "//移植" 就可以定位到 库 所需的依赖, 需要根据 您的环境实现这些…

微信公众号打通与登录的实现

今天实现一下与微信公众号进行对接&#xff0c;通过扫描二维码的方式来进行注册与登录&#xff0c;获取用户的微信唯一标识作为用户的username&#xff0c;下面我们开始编写。 骨架建立&#xff1a; 建包&#xff1a; 第一步还是先将骨架建好&#xff0c;与网关骨架差不多&a…

RTA_OS基础功能讲解 2.9-警报器

RTA_OS基础功能讲解 2.9-警报器 文章目录 RTA_OS基础功能讲解 2.9-警报器一、警报器简介二、警报器配置2.1 激活一个任务2.2 设置一个事件2.3 执行回调函数2.4 递增一个(软件)计数器三、警报器设置3.1 绝对警报3.1.1 单次触发3.1.2 周期触发3.1.3 在过去设置警报3.1.4 将绝对…

2_2、MFC对话框应用

对话框应用 模态与非模态对话框模态对话框弹出模态对话框创建模态对话框 非模态对话框 属性页对话框向导对话框一般属性页对话框 消息对话框函数原型函数返回值调用 文件对话框字体对话框获取字体对话框中所选字体选取字体样式并显示在编辑框中 颜色对话框获取取颜色对话框中所…

leetcode第709题:转换成小写字母

注意字符不仅有26个英文字母&#xff0c;还有特殊字符。特殊字符的话&#xff0c;原样输出。 public class Solution {public char toLowChar(char c){if(c>a&&c<z){return c;}else if(c>A&&c<Z){int n(int)c32;return (char)n;}return c;}publi…

12、云服务器上搭建环境

云服务器上搭建环境 12.1 选择一款远程连接工具(mobax) 有很多,比如mobax、xshll等等,我这里选择mobax,下载个免费版的即可 安装完成后,双击打开: 第一步,创建远程连接的用户,用户默认为root,密码为远程服务器的密码 第二步,输入远程公网IP,选择刚刚创建的用…

机器学习笔记 - 用于3D点云数据分类的Point Net的训练

一、数据集 ShapeNet 是一项持续不断的努力,旨在建立一个注释丰富的大型 3D 形状数据集。我们为世界各地的研究人员提供这些数据,以支持计算机图形学、计算机视觉、机器人技术和其他相关学科的研究。ShapeNet 是普林斯顿大学、斯坦福大学和 TTIC 研究人员的合作成果。 Shape…

Nginx配置详细解释:(6)实现反向代理服务器,动静分离,负载均衡

目录 单台反向代理 proxy_pass后面加不加/的问题&#xff1a; 反向代理动静分离 反向代理负载均衡 作为代理服务器是当客户端访问代理服务器时&#xff0c;代理服务器代理客户端去访问真实web服务器。proxy_pass; 用来设置将客户端请求转发给的后端服务器的主机。 需要模块…

【PPT教程】一键重置幻灯片背景的方法,新建幻灯片带默认背景

目的是替换18届的研电赛ppt背景为19届 这里写目录标题 1.设计->设置背景格式2.图片或纹理填充->插入3.选择需要替换为背景的照片4.点击下方的应用到全部 1.设计->设置背景格式 2.图片或纹理填充->插入 3.选择需要替换为背景的照片 4.点击下方的应用到全部 此时全部…

【Apache Doris】Compaction 原理 | 实践全析

【Apache Doris】Compaction 原理 | 实践全析 一、Compaction 前文概要二、Compaction 版本策略三、Compaction 类型说明四、Compaction 工程实现五、Compaction 生产实践 作者 &#xff5c; 俞剑波 一、Compaction 前文概要 LSM-Tree 简介 LSM-Tree&#xff08; Log Structu…

【Python推导式秘籍】:一行代码的艺术,高效数据处理之道

文章目录 &#x1f68b;Python推导式&#x1f680;一、列表推导式&#x1f308;1. 了解推导式❤️2. 实践&#x1f4a5;3. 总结 &#x1f680;二、字典推导式&#x1f308;1. 了解字典推导式❤️2. 实践&#x1f4a5;3. 总结 &#x1f680;三、集合推导式&#x1f308;1. 了解集…

目标检测数据集 - PCB板表面缺陷检测数据集下载「包含VOC、COCO、YOLO三种格式」

数据集介绍&#xff1a;PCB 板表面缺陷检测数据集&#xff0c;真实采集高质量 PCB 板表面含缺陷图片数据&#xff0c;数据集含多款不同 PCB 板高清表面图片数据&#xff0c;包括俯拍正拍、旋转拍摄姿态。数据标注标签包括 missing_hole、mouse_bite、open_circuit、short、spur…

00 - matlab m_map地学绘图工具安装及简单使用教程

00 - matlab m_map地学绘图工具安装及简单使用教程 0. 引言1. m_map工具的获取及配置过程2. 绘图示例3. 结语 0. 引言 m_map是MATLAB中的一个绘图工具包&#xff0c;用于绘制地图和地理数据。它提供了一系列函数&#xff0c;可以用来绘制地理投影、添加地理特征、绘制等值线图等…

上海2024年二手房最新成交历史数据

标签: 上海2024年二手房最新成交历史数据; 二手房; 时间: 2024年 上海市4月二手房网签套数:18230套 上海市5月二手房网签套数:19396套 上海市6月二手房网签套数: 6月1日:924套 6月2日:886套 6月3日:720套 6月4日:750套 6月5日:823套 6月6日:902套 6月7日:968套…

Javaweb8 数据库Mybatis+JDBC

Mybatis Dao层&#xff0c;用于简化JDBC开发 1步中的实体类 int类型一般用Integer &#xff1a;如果用int类型 默认值为0,会影响数据的判断,用Integer默认值是null,不会给数据的判断造成干扰 2.在application .properties里配置数据库的链接信息-四要素 #驱动类名称 #URL #用…

DistilBertModel模型的简单解释

前言 DistilBertModel((embeddings): Embeddings((word\_embeddings): Embedding(30522, 768, padding\_idx0)(position\_embeddings): Embedding(512, 768)(LayerNorm): LayerNorm((768,), eps1e-12, elementwise\_affineTrue)(dropout): Dropout(p\0.1, inplaceFalse))(trans…