当前位置:网站首页>Realization of RTSP Protocol

Realization of RTSP Protocol

2022-08-09 13:51:00 One Leaf Knows Autumn @qqy

写在前面

目前正在学习RTSP协议,偶然间发现在这篇文章非常好,故转载学习使用:RTSP协议的实现
All the code in this article comes from this article,Made some notes of my own based on that,Then something changed,The article will also explainsocket,Feel gnawed down,不懂rtsp,不知道socketIt is also readable,This period is also my own learning process.

创建套接字

Think about where we arevlc输入rtsp://127.0.0.1:8554what happened after?

在这种情况下,vlc其实是一个rtsp客户端,when entering thisurl后,vlc知道目的IP为127.0.0.1,目的端口号为8854,这时vlc会发起一个tcpConnect to connect to the server,After the connection is successful, the request starts to be sent,服务端响应

所以我们要写一个rtsp服务器,The first step is definitely to createtcp服务器

首先创建tcp套接字,绑定端口,监听

TCP函数定义:

/* * 创建tcp socket套接字 */
static int createTcpSocket()
{
    
    int sockfd;
    int on = 1;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
        return -1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));

    return sockfd;
}

UDP协议函数

/* * 创建udp socket套接字 */
static int createUdpSocket()
{
    
    int sockfd;
    int on = 1;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
        return -1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));

    return sockfd;
}

socket()函数

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);

domain参数

函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族.通信协议族在文件sys/socket.h中定义.

Several commonly used definitions are:

名称含义名称含义
PF_UNIX,PF_LOCAL本地通信PF_X25ITU-T X25 / ISO-8208协议
AF_INET,PF_INETIPv4 Internet协议PF_AX25Amateur radio AX.25
PF_INET6IPv6 Internet协议PF_ATMPVC原始ATM PVC访问
PF_IPXIPX-Novell协议PF_APPLETALKAppletalk
PF_NETLINK内核用户界面设备PF_PACKET底层包访问

Obviously this time it isIPv4 Internet协议,所以入参AF_INET.
在Linux系统中AF_和PF_是等价的.

type参数

函数socket()的参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK_DGRAM(数据包套接字)等.
其具体定义:

名称含义
SOCK_STREAMTcp连接,提供序列化的、可靠的、双向连接的字节流.支持带外数据传输
SOCK_DGRAM支持UDP连接(无连接状态的消息,不可靠,最大长度固定)
SOCK_SEQPACKET序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常.每次调用读系统调用时数据需要将全部数据读出
SOCK_RAWRAW类型,提供原始网络协议访问
SOCK_RDM提供可靠的数据报文,不过可能数据会有乱序
SOCK_PACKETThis is a dedicated type,It cannot be used in general programs

本文采用SOCK_STREAM入参TCP,采用SOCK_DGRAM入参UDP.

protocol参数

函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型.通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型.

当protocol为0时,会自动选择type类型对应的默认协议.

本文采用0入参

setsockopt()函数设置socket的状态

头文件:

#include <sys/types.h> 
#include <sys/socket.h>

定义函数:

int getsockopt(int fd, int level, int optname, void* optval, socklen_t* optlen);

参数fd

指定的socket对象,即上文提到的sockfd,是socketThe context of the socket

参数level

Represents the network layer to be read, 一般设成SOL_SOCKET 以存取socket 层,Its original definition is :

#define SOL_SOCKET 1

levelDefines which option will be used.通常情况下是SOL_SOCKET,means in usesocket选项.It can also set protocol options by setting a special protocol number

参数optname

Indicates which option status you want to get,Common parameter values ​​are shown below.

SO_DEBUG打开或关闭排错模式
SO_REUSEADDR允许在bind ()过程中本地地址可重复使用
SO_TYPE返回socket 形态
SO_ERROR返回socket 已发生的错误原因
SO_DONTROUTE送出的数据包不要利用路由设备来传输
SO_BROADCAST使用广播方式传送
SO_SNDBUFSet to send`Staging area size
SO_RCVBUF设置接收的Staging area size
SO_KEEPALIVE定期确定连线是否已终止
SO_OOBINLINE当接收到OOB 数据时会马上送至标准输入设备
SO_LINGER确保数据安全且可靠的传送出去.

本文采用SO_REUSEADDR

参数optval &参数optlen

参数 optval 代表欲设置的值, 参数optlen 则为optval 的长度.
指的是函数socket()的type值,即optval 设置为 1时,实际设置为SOCK_STREAM.

绑定地址和端口号

在创建完socketThe address and port number need to be bound after the socket,TCP时,The server must be bound to a fixed address and port number,This enables monitoring,The client can only respond when accessing this address and port number.
函数定义:

static int bindSocketAddr(int sockfd, const char* ip, int port)
{
    
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);

    if(bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0)
        return -1;

    return 0;
}

sockaddr_in 结构体

结构体定义:

#include <netinet/in.h>
/* Structure describing an Internet socket address. */
struct sockaddr_in
  {
    
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;	/* Port number. */
    struct in_addr sin_addr; /* Internet address. */

    /* Pad to size of `struct sockaddr'. */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

The mutual reference relationship in it can be viewed by yourself,There are mainly three parameters used here.
sin_family指代协议族,在socket编程中只能是AF_INET
sin_port存储端口号(使用网络字节顺序,htons转换)
sin_addr存储IP地址,使用in_addr这个数据结构,使用inet_addr转换

bind函数

函数定义:

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfdis the socket context mentioned above
addr指向套接字地址结构的指针
addrlen结构的大小

The address this example binds to is INADDR_ANY,端口号为8554

开始监听

函数定义:

#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog)

backlog

TCPThe connection is a process,It's not an instant link,So there may be multiple connections at the same time,There is a queue-like existence within the process to accommodate these requests,This parameter is to set the number of requests to accommodate,一般30以内.

创建RTP和RTCP的套接字

RTSPThe server is used to transmit audio and video data and informationRTP和RTCP,所以我们还要为RTP和RTCP创建UDP套接字,And bind the port number
创建套接字:

1、serverRtpSockfd = createUdpSocket();
2、serverRtcpSockfd = createUdpSocket();

The creation function here refers to the above.

绑定端口号:

bindSocketAddr(serverRtpSockfd, "0.0.0.0", SERVER_RTP_PORT);
bindSocketAddr(serverRtcpSockfd, "0.0.0.0", SERVER_RTCP_PORT);

For the same reason, refer to the functions mentioned above.
注:0.0.0.0指的是本机上的所有IPV4地址

开始accept等待客户端连接

函数定义:

/* * 开始accept等待客户端连接 * sockfd socket套接字上下文 * ip 地址 连接成功后通过ipThe pointer is returned to the clientip * port 端口号 连接成功后通过portThe pointer is returned to the clientport */
static int acceptClient(int sockfd, char* ip, int* port)
{
    
    int clientfd;
    socklen_t len = 0;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    len = sizeof(addr);

    clientfd = accept(sockfd, (struct sockaddr *)&addr, &len);
    if(clientfd < 0)
        return -1;
    
    strcpy(ip, inet_ntoa(addr.sin_addr));
    *port = ntohs(addr.sin_port);

    return clientfd;
}

accept函数

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd是由socket函数返回的套接字描述符,
参数addr和addrlen用来返回已连接的对端进程(客户端)的协议地址.
如果我们对客户端的协议地址不感兴趣,可以把arrd和addrlen均置为空指针

从addr内取出ip和portWhen it comes to network order,That is, the endianness is involved,所以用inet_ntoa(addr.sin_addr)与ntohs(addr.sin_port)will be taken out separatelyipwith port conversion order.

解析请求

当rtspAfter the client connects successfully, it will start sending requests,The server then needs to receive the client request and start parsing,Then take appropriate action
Please refer to another article for the format of the request:RTSP协议讲解

OPTIONS rtsp://127.0.0.1:8554/live RTSP/1.0\r\n
CSeq: 2\r\n
\r\n

DESCRIBE rtsp://127.0.0.1:8554/live RTSP/1.0\r\n
CSeq: 3\r\n
Accept: application/sdp\r\n
\r\n

SETUP rtsp://127.0.0.1:8554/live/track0 RTSP/1.0\r\n
CSeq: 4\r\n
Transport: RTP/AVP;unicast;client_port=54492-54493\r\n
\r\n

PLAY rtsp://127.0.0.1:8554/live RTSP/1.0\r\n
CSeq: 5\r\n
Session: 66334873\r\n
Range: npt=0.000-\r\n
\r\n

Here we do the easiest,First parse the first line to get the method,对于OPTIONS、DESCRIBE、PLAY、TEARDOWN我们只解析CSeq.对于SETUP,我们将client_port解析出来
So the first step we have to do is to parse the information in the request

  • 接收客户端数据
recvLen = recv(clientSockfd, rBuf, BUF_MAX_SIZE, 0);

A simple function is implemented heregetLineFromBuf,从buf中读取一行(\r\n)

  • Parse the first line of the request to get the method
sscanf(line, "%s %s %s\r\n", method, url, version);

sscanf函数定义:

#include <stdio.h>
 int sscanf (char *str, char * format [, argument, ...]);

sscanf()The function is used to read from a string指定格式的数据

str为要读取数据的字符串;
format为用户指定的格式;
argument为变量,用来保存读取到的数据
用起来和sprintf或者printf的格式很像,Only the effect and the scene are different.

  • 如果方法是SETUPthen re-analyzeclient_port
if(!strcmp(method, "SETUP"))
{
    
	sscanf(line, "Transport: RTP/AVP;unicast;client_port=%d-%d\r\n",
		&clientRtpPort, &clientRtcpPort);
}

After parsing the request command,The next step is to make different responses in different ways,如下:

if(!strcmp(method, "OPTIONS"))
{
    
	handleCmd_OPTIONS();
}
else if(!strcmp(method, "DESCRIBE"))
{
    
	handleCmd_DESCRIBE();
}
else if(!strcmp(method, "SETUP"))
{
    
	handleCmd_SETUP();
}
else if(!strcmp(method, "PLAY"))
{
    
	handleCmd_PLAY();
}
else if(!strcmp(method, "TEARDOWN"))
{
    
	handleCmd_TEARDOWN();
}

strcmp()函数

strcmp函数定义:

#include <string.h>
int strcmp(const char *s1, const char *s2);

strcmp() 用来比较字符串(区分大小写)
【参数】s1, s2 为需要比较的两个字符串.
Commonly used here is to compare whether two strings are the same,完全相同时返回0

DESCRIBE响应

DESCRIBE是客户端向服务器请求媒体信息,这是服务器需要回复sdp描述文件,The media in this example is H.264

  • sdp文件生成
sprintf(sdp, "v=0\r\n"
			"o=- 9%ld 1 IN IP4 %s\r\n"
			"t=0 0\r\n"
			"a=control:*\r\n"
			"m=video 0 RTP/AVP 96\r\n"
			"a=rtpmap:96 H264/90000\r\n"
			"a=control:track0\r\n",
			time(NULL), localIp);

Here is just a simple example

  • 回复
sprintf(sBuf, "RTSP/1.0 200 OK\r\n"
		"CSeq: %d\r\n"
		"Content-Base: %s\r\n"
		"Content-type: application/sdp\r\n"
		"Content-length: %d\r\n\r\n"
		"%s",
		cseq,
		url,
		strlen(sdp),
		sdp);
		
send(clientSockfd, sBuf, strlen(sBuf));

SETUP响应

SETUP是客户端请求建立会话连接,and sent the client'sRTP端口和RTCP端口,Then the server needs to reply to the server at this timeRTP端口和RTCP端口

  • 回复
sprintf(result, "RTSP/1.0 200 OK\r\n"
			"CSeq: %d\r\n"
			"Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
			"Session: 66334873\r\n"
			"\r\n",
			cseq,
			clientRtpPort,
			clientRtpPort+1,
			SERVER_RTP_PORT,
			SERVER_RTCP_PORT);

send(clientSockfd, sBuf, strlen(sBuf));

其中session id是随便写的,Just make sure it's unique when multiple sessions are connected
playAfter the response, it can be sent to the clientRTP端口发送RTP包了

PLAY响应

PLAYWhen the client requests playback from the server,At this time, the server starts to pass after replying to the requestsetup过程中创建的udp套接字发送RTP包

  • 回复
sprintf(result, "RTSP/1.0 200 OK\r\n"
				"CSeq: %d\r\n"
				"Range: npt=0.000-\r\n"
				"Session: 66334873; timeout=60\r\n\r\n",
				cseq);

send(clientSockfd, sBuf, strlen(sBuf));
  • 开始发送数据
    回复之后,Begin to specify to the clientRTP端口发送RTP包,如何发送RTP包,下篇文章再介绍

源码

/* * 作者:_JT_ * 博客:https://blog.csdn.net/weixin_42462202 */
 
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>

#define SERVER_PORT 8554 // 服务端 端口
#define SERVER_RTP_PORT 55532 // 服务端RTP 端口
#define SERVER_RTCP_PORT 55533 // 服务端RTCP 端口
#define BUF_MAX_SIZE (1024*1024) //最大存储buf size

/* * 创建tcp socket套接字 */
static int createTcpSocket()
{
    
    int sockfd;
    int on = 1;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
        return -1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));

    return sockfd;
}

/* * 创建udp socket套接字 */
static int createUdpSocket()
{
    
    int sockfd;
    int on = 1;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
        return -1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));

    return sockfd;
}

/* * 绑定地址和端口号 * sockfd socket套接字上下文 * ip 地址 * port 端口号 */
static int bindSocketAddr(int sockfd, const char* ip, int port)
{
    
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);

    if(bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0)
        return -1;

    return 0;
}

/* * 开始accept等待客户端连接 * sockfd socket套接字上下文 * ip 地址 连接成功后通过ipThe pointer is returned to the clientip * port 端口号 连接成功后通过portThe pointer is returned to the clientport */
static int acceptClient(int sockfd, char* ip, int* port)
{
    
    int clientfd;
    socklen_t len = 0;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    len = sizeof(addr);

    clientfd = accept(sockfd, (struct sockaddr *)&addr, &len);
    if(clientfd < 0)
        return -1;
    
    strcpy(ip, inet_ntoa(addr.sin_addr));
    *port = ntohs(addr.sin_port);

    return clientfd;
}

/* * 从bufCopy a row of data out of it,That is, monitor the newline at the end of the line * buf Received metadata * line A row of data is copied * return Returns the first new address after fetching the data,That is, the first address after the newline character */
static char* getLineFromBuf(char* buf, char* line)
{
    
    while(*buf != '\n')
    {
    
        *line = *buf;
        line++;
        buf++;
    }

    *line = '\n';
    ++line;
    *line = '\0';

    ++buf;
    return buf; 
}

/* * Processing command isOPTIONSThe required return data * result Enter the data to be parsed * cseq Play with output parsingcseqaddress of the data */
static int handleCmd_OPTIONS(char* result, int cseq)
{
    
    sprintf(result, "RTSP/1.0 200 OK\r\n"
                    "CSeq: %d\r\n"
                    "Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n"
                    "\r\n",
                    cseq);
                
    return 0;
}

/* * Processing command isDESCRIBEThe required return data * result Enter the data to be parsed * cseq Play with output parsingcseqaddress of the data * url Play with output parsingurladdress of the data */
static int handleCmd_DESCRIBE(char* result, int cseq, char* url)
{
    
    char sdp[500];
    char localIp[100];

    sscanf(url, "rtsp://%[^:]:", localIp);

    sprintf(sdp, "v=0\r\n"
                 "o=- 9%ld 1 IN IP4 %s\r\n"
                 "t=0 0\r\n"
                 "a=control:*\r\n"
                 "m=video 0 RTP/AVP 96\r\n"
                 "a=rtpmap:96 H264/90000\r\n"
                 "a=control:track0\r\n",
                 time(NULL), localIp);
    
    sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
                    "Content-Base: %s\r\n"
                    "Content-type: application/sdp\r\n"
                    "Content-length: %d\r\n\r\n"
                    "%s",
                    cseq,
                    url,
                    strlen(sdp),
                    sdp);
    
    return 0;
}

/* * Processing command isSETUPThe required return data * result Enter the data to be parsed * cseq Play with output parsingcseqaddress of the data * clientRtpPort Play with output parsingclientRtpPortaddress of the data */
static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort)
{
    
    sprintf(result, "RTSP/1.0 200 OK\r\n"
                    "CSeq: %d\r\n"
                    "Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
                    "Session: 66334873\r\n"
                    "\r\n",
                    cseq,
                    clientRtpPort,
                    clientRtpPort+1,
                    SERVER_RTP_PORT,
                    SERVER_RTCP_PORT);
    
    return 0;
}

/* * Processing command isPLAYThe required return data * result Enter the data to be parsed * cseq Play with output parsingcseqaddress of the data */
static int handleCmd_PLAY(char* result, int cseq)
{
    
    sprintf(result, "RTSP/1.0 200 OK\r\n"
                    "CSeq: %d\r\n"
                    "Range: npt=0.000-\r\n"
                    "Session: 66334873; timeout=60\r\n\r\n",
                    cseq);
    
    return 0;
}

/* * RTSP处理函数 */
static void doClient(int clientSockfd, const char* clientIP, int clientPort,
                        int serverRtpSockfd, int serverRtcpSockfd)
{
    
    char method[40]; //An array of load commands
    char url[100]; 
    char version[40];
    int cseq;
    int clientRtpPort, clientRtcpPort;
    char *bufPtr;
    char* rBuf = malloc(BUF_MAX_SIZE);
    char* sBuf = malloc(BUF_MAX_SIZE);
    char line[400];

    while(1)
    {
    
        int recvLen;

        recvLen = recv(clientSockfd, rBuf, BUF_MAX_SIZE, 0); 
        if(recvLen <= 0)
            goto out;

        rBuf[recvLen] = '\0';
        printf("---------------C->S--------------\n");
        printf("%s", rBuf);

        /* 解析方法 */
        bufPtr = getLineFromBuf(rBuf, line);
        if(sscanf(line, "%s %s %s\r\n", method, url, version) != 3)
        {
    
            printf("parse err\n");
            goto out;
        }

        /* Parse the serial number */
        bufPtr = getLineFromBuf(bufPtr, line);
        if(sscanf(line, "CSeq: %d\r\n", &cseq) != 1)
        {
    
            printf("parse err\n");
            goto out;
        }

        /* 如果是SETUP,Then analyze againclient_port */
        if(!strcmp(method, "SETUP"))
        {
    
            while(1)
            {
    
                bufPtr = getLineFromBuf(bufPtr, line);
                if(!strncmp(line, "Transport:", strlen("Transport:")))
                {
    
                    sscanf(line, "Transport: RTP/AVP;unicast;client_port=%d-%d\r\n",
                                    &clientRtpPort, &clientRtcpPort);
                    break;
                }
            }
        }

        if(!strcmp(method, "OPTIONS"))
        {
    
            if(handleCmd_OPTIONS(sBuf, cseq))
            {
    
                printf("failed to handle options\n");
                goto out;
            }
        }
        else if(!strcmp(method, "DESCRIBE"))
        {
    
            if(handleCmd_DESCRIBE(sBuf, cseq, url))
            {
    
                printf("failed to handle describe\n");
                goto out;
            }
        }
        else if(!strcmp(method, "SETUP"))
        {
    
            if(handleCmd_SETUP(sBuf, cseq, clientRtpPort))
            {
    
                printf("failed to handle setup\n");
                goto out;
            }
        }
        else if(!strcmp(method, "PLAY"))
        {
    
            if(handleCmd_PLAY(sBuf, cseq))
            {
    
                printf("failed to handle play\n");
                goto out;
            }
        }
        else
        {
    
            goto out;
        }

        printf("---------------S->C--------------\n");
        printf("%s", sBuf);
        send(clientSockfd, sBuf, strlen(sBuf), 0);
    }
out:
    close(clientSockfd);
    free(rBuf);
    free(sBuf);
}

int main(int argc, char* argv[])
{
    
    int serverSockfd;
    int serverRtpSockfd, serverRtcpSockfd;
    int ret;

    serverSockfd = createTcpSocket();
    if(serverSockfd < 0)
    {
    
        printf("failed to create tcp socket\n");
        return -1;
    }

    ret = bindSocketAddr(serverSockfd, "0.0.0.0", SERVER_PORT);
    if(ret < 0)
    {
    
        printf("failed to bind addr\n");
        return -1;
    }

    ret = listen(serverSockfd, 10);
    if(ret < 0)
    {
    
        printf("failed to listen\n");
        return -1;
    }

    serverRtpSockfd = createUdpSocket();
    serverRtcpSockfd = createUdpSocket();
    if(serverRtpSockfd < 0 || serverRtcpSockfd < 0)
    {
    
        printf("failed to create udp socket\n");
        return -1;
    }

    if(bindSocketAddr(serverRtpSockfd, "0.0.0.0", SERVER_RTP_PORT) < 0 ||
        bindSocketAddr(serverRtcpSockfd, "0.0.0.0", SERVER_RTCP_PORT) < 0)
    {
    
        printf("failed to bind addr\n");
        return -1;
    }

    printf("rtsp://127.0.0.1:%d\n", SERVER_PORT);

    while(1)
    {
    
        int clientSockfd;
        char clientIp[40];
        int clientPort;

        clientSockfd = acceptClient(serverSockfd, clientIp, &clientPort);
        if(clientSockfd < 0)
        {
    
            printf("failed to accept client\n");
            return -1;
        }

        printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);

        doClient(clientSockfd, clientIp, clientPort, serverRtpSockfd, serverRtcpSockfd);
    }

    return 0;
}
原网站

版权声明
本文为[One Leaf Knows Autumn @qqy]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/221/202208091244282450.html