RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)
=====================================================
RTMPdump(libRTMP) 源代码分析系列文章:
RTMPDump (libRTMP) 源代码分析2:解析RTMP地址——RTMP_ParseURL()
RTMPdump (libRTMP) 源代码分析3: AMF编码
RTMPdump (libRTMP) 源代码分析4: 连接第一步——握手 (HandShake)
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 (libRTMP)的整体的函数调用结构图如下图所示。
详细分析
书接上回: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
RTMPdump(libRTMP) 源代码分析 7: 建立一个流媒体连接 (NetStream部分 2)的更多相关文章
- RTMPdump(libRTMP) 源代码分析 6: 建立一个流媒体连接 (NetStream部分 1)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 5: 建立一个流媒体连接 (NetConnection部分)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- Spark源代码分析之中的一个:Job提交执行总流程概述
Spark是一个基于内存的分布式计算框架.执行在其上的应用程序,依照Action被划分为一个个Job.而Job提交执行的总流程.大致分为两个阶段: 1.Stage划分与提交 (1)Job依照RDD之间 ...
- RTMPdump(libRTMP)源代码分析 4: 连接第一步——握手(Hand Shake)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 10: 处理各种消息(Message)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 9: 接收消息(Message)(接收视音频数据)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- RTMPdump(libRTMP) 源代码分析 8: 发送消息(Message)
===================================================== RTMPdump(libRTMP) 源代码分析系列文章: RTMPdump 源代码分析 1: ...
- 转:RTMPDump源代码分析
0: 主要函数调用分析 rtmpdump 是一个用来处理 RTMP 流媒体的开源工具包,支持 rtmp://, rtmpt://, rtmpe://, rtmpte://, and rtmps://. ...
- Android在如何建立一个WebServer
今天老板交待任务最终完成了,感觉收获颇多,所以写一个关于它的记录,首先,看一下.老板的需求 需求: 希望移动端的用户标识(IMEI)和HTML页面的用户标识(Cookie)连接起来,当中HTML页面可 ...
随机推荐
- tomcat生命周期的管理——生命周期统一接口Lifecycle
我们知道Tomcat的架构设计是清晰的.模块化的,其拥有很多组件,假如我们要启动Tomcat,可以一个一个启动组件,但这样启动有很多缺点,不仅麻烦,而且容易漏了组件启动,还会对后面动态组件扩展带来麻烦 ...
- 带你深入理解STL之RBTree
最近一直忙于校招的笔试,STL的深入理解系列也耽搁了好几天,再加上!红黑树真的是超级超级难理解,超级超级复杂,参考了好多博客上的大神的理解才稍微明白一点,勉强入个门,下面请以一个菜鸟的角度跟着我一起学 ...
- Java提升篇之反射的原理(二)
Java提升篇之通过反射越过泛型检查 /* *问题:在一个ArrayList<Integer>对象中,在这个集合中添加一个字符串. */ 在我们还没有学反射前,遇到这个问题都是无法实现的, ...
- maven跳过单元测试的两个参数区别
maven在打包过程中需要执行单元测试.但有些时候单元测试已经通过只是想打包时,想跳过测试.maven提供了两个参数跳过测试:maven.test.skip=true 和skipTests. 例子 m ...
- 编译GDAL支持OpenCL使用GPU加速
前言 GDAL库中提供的gdalwarp支持各种高性能的图像重采样算法,图像重采样算法广泛应用于图像校正,重投影,裁切,镶嵌等算法中,而且对于这些算法来说,计算坐标变换的运算量是相当少的,绝大部分运算 ...
- 【NPR】漫谈轮廓线的渲染
写在前面 好久没写文章.最近在看<Real Time Rendering, third edition>这本书,看到了NPR这一章就想顺便记录下一些常见的轮廓线渲染的方法. 在非真实感渲染 ...
- velocity中加载模板文件的方式
velocity有多中种方式供我们去加载我们自定义的模板文件,下面详细的介绍使用的方法. 1.1.1. 加载classpath目录下的模板文件 使用classpath方式加载,是我们经常用到的一种方式 ...
- TCP的发送系列 — tcp_sendmsg()的实现(一)
主要内容:Socket发送函数在TCP层的实现 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 上一篇blog讲的是send().sendto().sen ...
- ubuntu opengl 开发
开发环境: eclipse,需要安装C++开发插件,在自带的源中查找安装C++开发工具包即可 下载安装gl库: sudo apt-get install libgl1-mesa-dev 下载安装glu ...
- Ubuntu下安装GTK环境
要生成C图形界面的程序,得安装GTK环境 安装GTK环境只要安装一个gnome-core-devel就可以了,里面集成了很多其他的包.除此之外还要转一些其他的 东西,如libglib2.0 ...