文档章节

RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)

雷霄骅
 雷霄骅
发布于 2014/08/16 13:50
字数 2406
阅读 43
收藏 0

注:此前写了一些列的分析RTMPdump(libRTMP)源代码的文章,在此列一个列表:
RTMPdump 源代码分析 1: main()函数
RTMPDump(libRTMP)源代码分析 2:解析RTMP地址——RTMP_ParseURL()
RTMPdump(libRTMP) 源代码分析 3: AMF编码
RTMPdump(libRTMP)源代码分析 4: 连接第一步——握手(Hand Shake)
RTMPdump(libRTMP) 源代码分析 5: 建立一个流媒体连接 (NetConnection部分)
RTMPdump(libRTMP) 源代码分析 6: 建立一个流媒体连接 (NetStream部分 1)
RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)
RTMPdump(libRTMP) 源代码分析 8: 发送消息(Message)
RTMPdump(libRTMP) 源代码分析 9: 接收消息(Message)(接收视音频数据)
RTMPdump(libRTMP) 源代码分析 10: 处理各种消息(Message)

===============================


书接上回:RTMPdump 源代码分析 6: 建立一个流媒体连接 (NetStream部分 1)

上回说到,有两个函数尤为重要:

RTMP_ReadPacket()

RTMP_ClientPacket()

而且分析了第一个函数。现在我们再来看看第二个函数吧。 第二个函数的主要作用是:处理消息(Message),并做出响应。

先把带注释的代码贴上:

//处理接收到的Chunk
int
RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
{
  int bHasMediaPacket = 0;
  switch (packet->m_packetType)
    {
	//RTMP消息类型ID=1,设置块大小
    case 0x01:
      /* chunk size */
		//----------------
		r->dlg->AppendCInfo("处理收到的数据。消息 Set Chunk Size (typeID=1)。");
		//-----------------------------
		RTMP_LogPrintf("处理消息 Set Chunk Size (typeID=1)\n");
      HandleChangeChunkSize(r, packet);
      break;
	//RTMP消息类型ID=3,致谢
    case 0x03:
      /* bytes read report */
      RTMP_Log(RTMP_LOGDEBUG, "%s, received: bytes read report", __FUNCTION__);
      break;
	//RTMP消息类型ID=4,用户控制
    case 0x04:
      /* ctrl */
		//----------------
		r->dlg->AppendCInfo("处理收到的数据。消息 User Control (typeID=4)。");
		//-----------------------------
		RTMP_LogPrintf("处理消息 User Control (typeID=4)\n");
      HandleCtrl(r, packet);
      break;
	//RTMP消息类型ID=5
    case 0x05:
      /* server bw */
		//----------------
		r->dlg->AppendCInfo("处理收到的数据。消息 Window Acknowledgement Size (typeID=5)。");
		//-----------------------------
		RTMP_LogPrintf("处理消息 Window Acknowledgement Size (typeID=5)\n");
      HandleServerBW(r, packet);
      break;
	//RTMP消息类型ID=6
    case 0x06:
      /* client bw */
		//----------------
		r->dlg->AppendCInfo("处理收到的数据。消息 Set Peer Bandwidth (typeID=6)。");
		//-----------------------------
		RTMP_LogPrintf("处理消息 Set Peer Bandwidth (typeID=6)\n");
      HandleClientBW(r, packet);
      break;
	//RTMP消息类型ID=8,音频数据
    case 0x08:
      /* audio data */
      /*RTMP_Log(RTMP_LOGDEBUG, "%s, received: audio %lu bytes", __FUNCTION__, packet.m_nBodySize); */
      HandleAudio(r, packet);
      bHasMediaPacket = 1;
      if (!r->m_mediaChannel)
	r->m_mediaChannel = packet->m_nChannel;
      if (!r->m_pausing)
	r->m_mediaStamp = packet->m_nTimeStamp;
      break;
	//RTMP消息类型ID=9,视频数据
    case 0x09:
      /* video data */
      /*RTMP_Log(RTMP_LOGDEBUG, "%s, received: video %lu bytes", __FUNCTION__, packet.m_nBodySize); */
      HandleVideo(r, packet);
      bHasMediaPacket = 1;
      if (!r->m_mediaChannel)
	r->m_mediaChannel = packet->m_nChannel;
      if (!r->m_pausing)
	r->m_mediaStamp = packet->m_nTimeStamp;
      break;
	//RTMP消息类型ID=15,AMF3编码,忽略
    case 0x0F:			/* flex stream send */
      RTMP_Log(RTMP_LOGDEBUG,
	  "%s, flex stream send, size %lu bytes, not supported, ignoring",
	  __FUNCTION__, packet->m_nBodySize);
      break;
	//RTMP消息类型ID=16,AMF3编码,忽略
    case 0x10:			/* flex shared object */
      RTMP_Log(RTMP_LOGDEBUG,
	  "%s, flex shared object, size %lu bytes, not supported, ignoring",
	  __FUNCTION__, packet->m_nBodySize);
      break;
	//RTMP消息类型ID=17,AMF3编码,忽略
    case 0x11:			/* flex message */
      {
	RTMP_Log(RTMP_LOGDEBUG,
	    "%s, flex message, size %lu bytes, not fully supported",
	    __FUNCTION__, packet->m_nBodySize);
	/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */

	/* some DEBUG code */
#if 0
	   RTMP_LIB_AMFObject obj;
	   int nRes = obj.Decode(packet.m_body+1, packet.m_nBodySize-1);
	   if(nRes < 0) {
	   RTMP_Log(RTMP_LOGERROR, "%s, error decoding AMF3 packet", __FUNCTION__);
	   /*return; */
	   }

	   obj.Dump();
#endif

	if (HandleInvoke(r, packet->m_body + 1, packet->m_nBodySize - 1) == 1)
	  bHasMediaPacket = 2;
	break;
      }
	//RTMP消息类型ID=18,AMF0编码,数据消息
    case 0x12:
      /* metadata (notify) */

      RTMP_Log(RTMP_LOGDEBUG, "%s, received: notify %lu bytes", __FUNCTION__,
	  packet->m_nBodySize);
	  //处理元数据,暂时注释
	  /*
      if (HandleMetadata(r, packet->m_body, packet->m_nBodySize))
	bHasMediaPacket = 1;
      break;
	  */
	//RTMP消息类型ID=19,AMF0编码,忽略
    case 0x13:
      RTMP_Log(RTMP_LOGDEBUG, "%s, shared object, not supported, ignoring",
	  __FUNCTION__);
      break;
	//RTMP消息类型ID=20,AMF0编码,命令消息
	//处理命令消息!
    case 0x14:
		//----------------
		r->dlg->AppendCInfo("处理收到的数据。消息 命令 (AMF0编码) (typeID=20)。");
		//-----------------------------
      /* invoke */
      RTMP_Log(RTMP_LOGDEBUG, "%s, received: invoke %lu bytes", __FUNCTION__,
	  packet->m_nBodySize);
	  RTMP_LogPrintf("处理命令消息 (typeID=20,AMF0编码)\n");
      /*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */

      if (HandleInvoke(r, packet->m_body, packet->m_nBodySize) == 1)
	bHasMediaPacket = 2;
      break;
	//RTMP消息类型ID=22
    case 0x16:
      {
	/* go through FLV packets and handle metadata packets */
	unsigned int pos = 0;
	uint32_t nTimeStamp = packet->m_nTimeStamp;

	while (pos + 11 < packet->m_nBodySize)
	  {
	    uint32_t dataSize = AMF_DecodeInt24(packet->m_body + pos + 1);	/* size without header (11) and prevTagSize (4) */

	    if (pos + 11 + dataSize + 4 > packet->m_nBodySize)
	      {
		RTMP_Log(RTMP_LOGWARNING, "Stream corrupt?!");
		break;
	      }
	    if (packet->m_body[pos] == 0x12)
	      {
		HandleMetadata(r, packet->m_body + pos + 11, dataSize);
	      }
	    else if (packet->m_body[pos] == 8 || packet->m_body[pos] == 9)
	      {
		nTimeStamp = AMF_DecodeInt24(packet->m_body + pos + 4);
		nTimeStamp |= (packet->m_body[pos + 7] << 24);
	      }
	    pos += (11 + dataSize + 4);
	  }
	if (!r->m_pausing)
	  r->m_mediaStamp = nTimeStamp;

	/* FLV tag(s) */
	/*RTMP_Log(RTMP_LOGDEBUG, "%s, received: FLV tag(s) %lu bytes", __FUNCTION__, packet.m_nBodySize); */
	bHasMediaPacket = 1;
	break;
      }
    default:
      RTMP_Log(RTMP_LOGDEBUG, "%s, unknown packet type received: 0x%02x", __FUNCTION__,
	  packet->m_packetType);
#ifdef _DEBUG
      RTMP_LogHex(RTMP_LOGDEBUG, (const uint8_t *)packet->m_body, packet->m_nBodySize);
#endif
    }

  return bHasMediaPacket;
}


里面注释的比较多,可以看出,大体的思路是,根据接收到的消息(Message)类型的不同,做出不同的响应。例如收到的消息类型为0x01,那么就是设置块(Chunk)大小的协议,那么就调用相应的函数进行处理。

因此,本函数可以说是程序的灵魂,收到的各种命令消息都要经过本函数的判断决定调用哪个函数进行相应的处理。

在这里注意一下消息类型为0x14的消息,即消息类型ID为20的消息,是AMF0编码的命令消息。这在RTMP连接中是非常常见的,比如说各种控制命令:播放,暂停,停止等等。我们来仔细看看它的调用。

可以发现它调用了HandleInvoke()函数来处理服务器发来的AMF0编码的命令,来看看细节:

/* Returns 0 for OK/Failed/error, 1 for 'Stop or Complete' */
static int
HandleInvoke(RTMP *r, const char *body, unsigned int nBodySize)
{
  AMFObject obj;
  AVal method;
  int txn;
  int ret = 0, nRes;
  if (body[0] != 0x02)		/* make sure it is a string method name we start with */
    {
      RTMP_Log(RTMP_LOGWARNING, "%s, Sanity failed. no string method in invoke packet",
	  __FUNCTION__);
      return 0;
    }

  nRes = AMF_Decode(&obj, body, nBodySize, FALSE);
  if (nRes < 0)
    {
      RTMP_Log(RTMP_LOGERROR, "%s, error decoding invoke packet", __FUNCTION__);
      return 0;
    }

  AMF_Dump(&obj);
  AMFProp_GetString(AMF_GetProp(&obj, NULL, 0), &method);
  txn = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 1));
  RTMP_Log(RTMP_LOGDEBUG, "%s, server invoking <%s>", __FUNCTION__, method.av_val);

  if (AVMATCH(&method, &av__result))
    {
      AVal methodInvoked = {0};
      int i;

      for (i=0; i<r->m_numCalls; i++) {
  	if (r->m_methodCalls[i].num == txn) {
	  methodInvoked = r->m_methodCalls[i].name;
	  AV_erase(r->m_methodCalls, &r->m_numCalls, i, FALSE);
	  break;
	}
      }
      if (!methodInvoked.av_val) {
        RTMP_Log(RTMP_LOGDEBUG, "%s, received result id %d without matching request",
	  __FUNCTION__, txn);
	goto leave;
      }
	  //----------------
	  char temp_str[100];
	  sprintf(temp_str,"接收数据。消息 %s 的 Result",methodInvoked.av_val);
	  r->dlg->AppendCInfo(temp_str);
	  //-----------------------------
      RTMP_Log(RTMP_LOGDEBUG, "%s, received result for method call <%s>", __FUNCTION__,
	  methodInvoked.av_val);

      if (AVMATCH(&methodInvoked, &av_connect))
	{
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","Result (Connect)");
		//-----------------------------
	  if (r->Link.token.av_len)
	    {
	      AMFObjectProperty p;
	      if (RTMP_FindFirstMatchingProperty(&obj, &av_secureToken, &p))
		{
		  DecodeTEA(&r->Link.token, &p.p_vu.p_aval);
		  SendSecureTokenResponse(r, &p.p_vu.p_aval);
		}
	    }
	  if (r->Link.protocol & RTMP_FEATURE_WRITE)
	    {
	      SendReleaseStream(r);
	      SendFCPublish(r);
	    }
	  else
	    {
			//----------------
			r->dlg->AppendCInfo("发送数据。消息 Window Acknowledgement Size (typeID=5)。");
			//-----------------------------
			RTMP_LogPrintf("发送消息Window Acknowledgement Size(typeID=5)\n");
	      RTMP_SendServerBW(r);
	      RTMP_SendCtrl(r, 3, 0, 300);
	    }
	  //----------------
	  r->dlg->AppendCInfo("发送数据。消息 命令 (typeID=20) (CreateStream)。");
	  //-----------------------------
	  RTMP_LogPrintf("发送命令消息“CreateStream” (typeID=20)\n");
	  RTMP_SendCreateStream(r);

	  if (!(r->Link.protocol & RTMP_FEATURE_WRITE))
	    {
	      /* Send the FCSubscribe if live stream or if subscribepath is set */
	      if (r->Link.subscribepath.av_len)
	        SendFCSubscribe(r, &r->Link.subscribepath);
	      else if (r->Link.lFlags & RTMP_LF_LIVE)
	        SendFCSubscribe(r, &r->Link.playpath);
	    }
	}
      else if (AVMATCH(&methodInvoked, &av_createStream))
	{
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","Result (CreateStream)");
		//-----------------------------
	  r->m_stream_id = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));

	  if (r->Link.protocol & RTMP_FEATURE_WRITE)
	    {
	      SendPublish(r);
	    }
	  else
	    {
	      if (r->Link.lFlags & RTMP_LF_PLST)
	        SendPlaylist(r);
		  //----------------
		  r->dlg->AppendCInfo("发送数据。消息 命令 (typeID=20) (Play)。");
		  //-----------------------------
		  RTMP_LogPrintf("发送命令消息“play” (typeID=20)\n");
	      SendPlay(r);
	      RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);
	    }
	}
      else if (AVMATCH(&methodInvoked, &av_play) ||
      	AVMATCH(&methodInvoked, &av_publish))
	{
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","Result (Play or Publish)");
		//-----------------------------
	  r->m_bPlaying = TRUE;
	}
      free(methodInvoked.av_val);
    }
  else if (AVMATCH(&method, &av_onBWDone))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","onBWDone");
		//-----------------------------
	  if (!r->m_nBWCheckCounter)
        SendCheckBW(r);
    }
  else if (AVMATCH(&method, &av_onFCSubscribe))
    {
      /* SendOnFCSubscribe(); */
    }
  else if (AVMATCH(&method, &av_onFCUnsubscribe))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","onFCUnsubscribe");
		//-----------------------------
      RTMP_Close(r);
      ret = 1;
    }
  else if (AVMATCH(&method, &av_ping))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","Ping");
		//-----------------------------
      SendPong(r, txn);
    }
  else if (AVMATCH(&method, &av__onbwcheck))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","onBWcheck");
		//-----------------------------
      SendCheckBWResult(r, txn);
    }
  else if (AVMATCH(&method, &av__onbwdone))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","onBWdone");
		//-----------------------------
      int i;
      for (i = 0; i < r->m_numCalls; i++)
	if (AVMATCH(&r->m_methodCalls[i].name, &av__checkbw))
	  {
	    AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
	    break;
	  }
    }
  else if (AVMATCH(&method, &av__error))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","error");
		//-----------------------------
      RTMP_Log(RTMP_LOGERROR, "rtmp server sent error");
    }
  else if (AVMATCH(&method, &av_close))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","close");
		//-----------------------------
      RTMP_Log(RTMP_LOGERROR, "rtmp server requested close");
      RTMP_Close(r);
    }
  else if (AVMATCH(&method, &av_onStatus))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","onStatus");
		//-----------------------------
      AMFObject obj2;
      AVal code, level;
      AMFProp_GetObject(AMF_GetProp(&obj, NULL, 3), &obj2);
      AMFProp_GetString(AMF_GetProp(&obj2, &av_code, -1), &code);
      AMFProp_GetString(AMF_GetProp(&obj2, &av_level, -1), &level);

      RTMP_Log(RTMP_LOGDEBUG, "%s, onStatus: %s", __FUNCTION__, code.av_val);
      if (AVMATCH(&code, &av_NetStream_Failed)
	  || AVMATCH(&code, &av_NetStream_Play_Failed)
	  || AVMATCH(&code, &av_NetStream_Play_StreamNotFound)
	  || AVMATCH(&code, &av_NetConnection_Connect_InvalidApp))
	{
	  r->m_stream_id = -1;
	  RTMP_Close(r);
	  RTMP_Log(RTMP_LOGERROR, "Closing connection: %s", code.av_val);
	}

      else if (AVMATCH(&code, &av_NetStream_Play_Start))
	{
	  int i;
	  r->m_bPlaying = TRUE;
	  for (i = 0; i < r->m_numCalls; i++)
	    {
	      if (AVMATCH(&r->m_methodCalls[i].name, &av_play))
		{
		  AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
		  break;
		}
	    }
	}

      else if (AVMATCH(&code, &av_NetStream_Publish_Start))
	{
	  int i;
	  r->m_bPlaying = TRUE;
	  for (i = 0; i < r->m_numCalls; i++)
	    {
	      if (AVMATCH(&r->m_methodCalls[i].name, &av_publish))
		{
		  AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
		  break;
		}
	    }
	}

      /* Return 1 if this is a Play.Complete or Play.Stop */
      else if (AVMATCH(&code, &av_NetStream_Play_Complete)
	  || AVMATCH(&code, &av_NetStream_Play_Stop)
	  || AVMATCH(&code, &av_NetStream_Play_UnpublishNotify))
	{
	  RTMP_Close(r);
	  ret = 1;
	}

      else if (AVMATCH(&code, &av_NetStream_Seek_Notify))
        {
	  r->m_read.flags &= ~RTMP_READ_SEEKING;
	}

      else if (AVMATCH(&code, &av_NetStream_Pause_Notify))
        {
	  if (r->m_pausing == 1 || r->m_pausing == 2)
	  {
	    RTMP_SendPause(r, FALSE, r->m_pauseStamp);
	    r->m_pausing = 3;
	  }
	}
    }
  else if (AVMATCH(&method, &av_playlist_ready))
    {
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","playlist_ready");
		//-----------------------------
      int i;
      for (i = 0; i < r->m_numCalls; i++)
        {
          if (AVMATCH(&r->m_methodCalls[i].name, &av_set_playlist))
	    {
	      AV_erase(r->m_methodCalls, &r->m_numCalls, i, TRUE);
	      break;
	    }
        }
    }
  else
    {

    }
leave:
  AMF_Reset(&obj);
  return ret;
}

int
RTMP_FindFirstMatchingProperty(AMFObject *obj, const AVal *name,
			       AMFObjectProperty * p)
{
  int n;
  /* this is a small object search to locate the "duration" property */
  for (n = 0; n < obj->o_num; n++)
    {
      AMFObjectProperty *prop = AMF_GetProp(obj, NULL, n);

      if (AVMATCH(&prop->p_name, name))
	{
	  *p = *prop;
	  return TRUE;
	}

      if (prop->p_type == AMF_OBJECT)
	{
	  if (RTMP_FindFirstMatchingProperty(&prop->p_vu.p_object, name, p))
	    return TRUE;
	}
    }
  return FALSE;
}


该函数主要做了以下几步:

1.调用AMF_Decode()解码AMF命令数据

2.调用AMFProp_GetString()获取具体命令的字符串

3.调用AVMATCH()比较字符串,不同的命令做不同的处理,例如以下几个:

AVMATCH(&methodInvoked, &av_connect)
AVMATCH(&methodInvoked, &av_createStream)
AVMATCH(&methodInvoked, &av_play)
AVMATCH(&methodInvoked, &av_publish)
AVMATCH(&method, &av_onBWDone)


等等,不一一例举了

具体的处理过程如下所示。在这里说一个“建立网络流”(createStream)的例子,通常发生在建立网络连接(NetConnection)之后,播放(Play)之前。

else if (AVMATCH(&methodInvoked, &av_createStream))
	{
		//----------------
		r->dlg->AppendMLInfo(20,0,"命令消息","Result (CreateStream)");
		//-----------------------------
	  r->m_stream_id = (int)AMFProp_GetNumber(AMF_GetProp(&obj, NULL, 3));

	  if (r->Link.protocol & RTMP_FEATURE_WRITE)
	    {
	      SendPublish(r);
	    }
	  else
	    {
	      if (r->Link.lFlags & RTMP_LF_PLST)
	        SendPlaylist(r);
		  //----------------
		  r->dlg->AppendCInfo("发送数据。消息 命令 (typeID=20) (Play)。");
		  //-----------------------------
		  RTMP_LogPrintf("发送命令消息“play” (typeID=20)\n");
	      SendPlay(r);
	      RTMP_SendCtrl(r, 3, r->m_stream_id, r->m_nBufferMS);
	    }
	}


由代码可见,程序先获取了stream_id,然后发送了两个消息(Message),分别是SendPlaylist()和SendPlay(),用于获取播放列表,以及开始播放流媒体数据。

rtmpdump源代码(Linux):http://download.csdn.net/detail/leixiaohua1020/6376561

rtmpdump源代码(VC 2005 工程):http://download.csdn.net/detail/leixiaohua1020/6563163


本文转载自:http://blog.csdn.net/leixiaohua1020/article/details/12958617

雷霄骅
粉丝 211
博文 419
码字总数 2129
作品 4
朝阳
程序员
私信 提问
安装rtmpdump,提示openssl没安装

最近在用openresty开发基于RTMP协议的流媒体业务,想装一个RTMPdump玩玩,但我的OS明明已经装了openssl,但在编译rtmpdump的时候仍然提示找不到openssl相关的库 make[1]: Entering director...

Teomc.Xbc
2017/08/07
297
1
大神们,做视频直播,RTMP协议一般都用什么开源库啊,给个建议吧?

我是用Linux c++基于rtmp协议做视频直播服务库,不知道用什么开源库好,希望各位大神给个建议,谢谢啦!另外RTMPDump、nginx-rtmp-module是rtmp开源库吗?librtmp和RTMPDump是什么关系,是一...

bensondawn
2016/12/26
1K
2
[总结]RTMP流媒体技术零基础学习方法

本文主要总结一些我在学习RTMP流媒体技术过程中积累的经验。也为后来学习RTMP流媒体技术的人们一个参考。本文力图从简到难,循序渐进的介绍RTMP流媒体技术的方方面面,先从应用说起,逐步深化...

leixiaohua1020
2013/11/18
0
0
Android 音视频开发入门指南(2)

GLSurfaceView是OpenGL中的一个类,也是可以预览Camera的,而且在预览Camera上有其独到之处。独到之处在哪?当使用Surfaceview无能为力、痛不欲生时就只有使用GLSurfaceView了,它能够真正做...

shareus
2018/04/23
0
0
ffmpeg(软)编码推流 - Android

-- ffmpeg推流:用到了ffmpeg库,以及编码视频的x264,编码音频的fdk-aac,推流使用的rtmp等 acc:RTMP的音频格式;flv: RTMP的视频格式; -- 视频流中的几个参数: public static final in...

desaco
2018/10/14
0
0

没有更多内容

加载失败,请刷新页面

加载更多

医疗项目pc端后台页面用vue重构整个html页面

页面源码是vue

xintaiideas
33分钟前
2
0
领域驱动中的“贫血症和失忆症” --实践领域驱动--原文

贫血症严重危害着人类健康,并且伴随有危险的副作用。当贫血领域对象被首次提出来时,它并不是一个博得赞美的词汇,它描述的是一个缺少内在行为领域对象。奇怪的是,人们对于贫血领域对象的态...

还仙
36分钟前
5
0
条码打印软件中标签预览正常打印无反应怎么解决

在使用条码打印软件制作标签时,有客户反馈,标签打印预览正常的,但是打印无反应,咨询是怎么回事?今天针对这个情况,可以参考以下方法进行解决。 一、预览正常情况下,打印没反应 (1)在条码...

中琅软件
46分钟前
5
0
判断字符串的时候

判断字符串的时候一定把常量房前边, //报警程度 String leve = vo.getDeviceAlertDeal().getWarnLevel(); if(("0").equals(leve)) { row.add("无报警"); }else if(("1").equals(leve)) { ro......

简小姐
46分钟前
7
0
Linux maven3.6.2 install

PS:安装 maven 之前请先安装 jdk 1.安装 wget 命令(安装过就不用了) yum -y install wget 2.寻找需要的 maven 版本 https://maven.apache.org/download.cgi 3.进入 /var/local 文件夹 cd...

东方神祇
49分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部