完成端口学习之简易聊天室
来源:互联网 发布:激活软件的危害 编辑:程序博客网 时间:2024/06/05 10:18
严格的来说吧,这个算是我学习C++后第一个写的不成熟的小程序,现在还没有毕业,一切的东西都算是学习中的小小动手玩乐。
这个是基于SOCKET完成端口而实现的C/S简易聊天工具,没有前端界面,就代码而言应该来说是很冗余和漏洞百出的。贴出来的目的在于将来有所成长后,回头看看曾经写的所谓“学生式的代码”。
这份代码也是具体学了小猪前辈的代码而有所得的东西。
原文:http://blog.csdn.net/piggyxp/article/details/6922277
具体的介绍就不多说了,毕竟这是写给以后的自己看的。
大体思路为:
1、构建IO操作结构体以及对应的数组
2、构建Socket操作的结构体以及对应的数组
3、用AcceptEx指针来获取相应的客户端信息
1.对于结构体的定义
在IO操作结构体当中,定义重叠结构体,然后再后面接上每个网络操作对应的Socket以及缓冲区等信息。
typedef struct _PER_IO_CONTEXT{ OVERLAPPED m_Overlapped; //每个socket有一个重叠结构 SOCKET m_SocketAccept; //这个网络操作对应的socket WSABUF m_wsaBuf; //缓冲区 char m_szBuffer[MAX_BUFFER_LEN]; //WSABUF存放字符的缓冲区 OPERATION_TPYE m_OpType; //枚举对象,标识网络操作的类型 //初始化资源 _PER_IO_CONTEXT() { ZeroMemory(&m_Overlapped, sizeof(m_Overlapped)); ZeroMemory(m_szBuffer, MAX_BUFFER_LEN); m_SocketAccept = INVALID_SOCKET; m_wsaBuf.buf = m_szBuffer; m_wsaBuf.len = MAX_BUFFER_LEN; m_OpType = NONE; } //释放socket资源 ~_PER_IO_CONTEXT() { if (m_SocketAccept != INVALID_SOCKET) { ZeroMemory(&m_Overlapped, sizeof(m_Overlapped)); ZeroMemory(&m_szBuffer, MAX_BUFFER_LEN); m_wsaBuf.buf = m_szBuffer; m_wsaBuf.len = MAX_BUFFER_LEN; m_OpType = NONE; } } // 重置缓冲区内容 void ResetBuffer() { ZeroMemory(m_szBuffer, MAX_BUFFER_LEN); }}PER_IO_CONTEXT;
然后就是Socket对应的结构体了
typedef struct _PER_SOCKET_CONTEXT{ SOCKET m_Socket; //每个客户端连接的socket SOCKADDR_IN m_ClientAddr; //每个客户端的地址 char m_username[40]; //存放用户名 //初始化 _PER_SOCKET_CONTEXT() { m_Socket = INVALID_SOCKET; //将m_ClientAddr这个位置后面的m_ClientAddr长度个字节用0填上 //即对m_ClientAddr进行清零 memset(&m_ClientAddr, 0, sizeof(m_ClientAddr)); } //释放资源 ~_PER_SOCKET_CONTEXT() { if (m_Socket != INVALID_SOCKET) { closesocket(m_Socket); m_Socket = INVALID_SOCKET; } }}PER_SOCKET_CONTEXT;
当然,每个结构体也有对应的操作,因为对于代码编程理解有限,所以采用比较笨拙的方式定义相应的方法
class PER_IO_CONTEXT_ARR{private: //创建io结构体数组,用来存放每个socket对应的io操作 PER_IO_CONTEXT *IO_CONTEXT_ARR[2048];public: int num=0; //计数 //获取编号 PER_IO_CONTEXT * GetARR(int i) { return IO_CONTEXT_ARR[i]; } //循环遍历IO操作数组,通过遍历所有位置,如果为0.则表示可以存放新的IO操作,后面只需要将数组全部填0,就可以初始化数组,并且在移除IO操作后,可以在原来位置上放心的IO操作。 PER_IO_CONTEXT* GetNewIoContext() { for (int i = 0; i < 2048; i++) { //如果某一个IO_CONTEXT_ARRAY[i]为0,表示哪一个位可以放入PER_IO_CONTEXT if (IO_CONTEXT_ARR[i] == 0) { IO_CONTEXT_ARR[i] = new PER_IO_CONTEXT(); num++; return IO_CONTEXT_ARR[i]; } } } //如果IO操作数组中某个io操作完成,那么移除该IO操作,并且用0替换该位置的值,以方便新的IO操作存放 //新增了一个IO操作,NUM++ 所以只需要循环遍历前num个成员就可以了 void RemoveContext(PER_IO_CONTEXT * RContext) { for (int i = 0; i < num; i++) { if (IO_CONTEXT_ARR[i] == RContext) { IO_CONTEXT_ARR[i]->~_PER_IO_CONTEXT(); IO_CONTEXT_ARR[i] = 0; //移除一个IO操作,计数-1 num--; break; } } return; }};
class PER_SOCKET_CONTEXT_ARR{private: //创建socket结构体数组 PER_SOCKET_CONTEXT * SOCKET_CONTEXT_ARR[2048];public: int num = 0; //计数 //循环判定socket结构体数组中那个位置为0,表示该位置可以存放新的socket信息,同IO操作数组一样,初始化后,就可以从0位置开始存放,并且因为和操作是一对一关系,因此socket结构体数组和io操作数组下标应该是对应关系,这样在后续相关操作过程中,通过下标查询相应的数组,就可以得到例如客户端的地址,端口,和相对应的socket。 PER_SOCKET_CONTEXT* GetNewSocketContext(SOCKADDR_IN* addr, char *u) { for (int i = 0; i < 2048; i++) { //如果某个位置上的值为0,表示该位置可以存放新的socket结构体信息 if (SOCKET_CONTEXT_ARR[i] == 0) { SOCKET_CONTEXT_ARR[num] = new PER_SOCKET_CONTEXT(); //该位置存放新的socket结构体信息,同时我们也把客户端的相应信息写入进去 //将传入进来的addr拷贝进SOCKET_CONTEXT_ARR指针指向的位置,长度为地址长度 memcpy(&(SOCKET_CONTEXT_ARR[num]->m_ClientAddr),addr,sizeof(SOCKADDR_IN)); //将传入方法的名字字符串拷贝进去 strcpy(SOCKET_CONTEXT_ARR[num]->m_username,u); //存入一个socket结构体数组信息,计数+1 num++; //返回值应该为当前socket结构体数组位置,而计数+1是为了新的socket结构体数组信息, return SOCKET_CONTEXT_ARR[num - 1]; } } } PER_SOCKET_CONTEXT * getARR(int i) { //获取当前socket结构体数组位置 return SOCKET_CONTEXT_ARR[i]; } //这里要新建一个方法,虽然有了判断位置是否可以存放数据,但是我们缺少一个通过计数值向SOCKET_CONTEXT_ARR中存放socket,addr,name的操作 void AddSocketArray(SOCKET s, SOCKADDR_IN *addr, char *u) { SOCKET_CONTEXT_ARR[num] = new PER_SOCKET_CONTEXT(); SOCKET_CONTEXT_ARR[num]->m_Socket = s; memcpy(&(SOCKET_CONTEXT_ARR[num]->m_ClientAddr),addr,sizeof(SOCKADDR_IN)); strcpy(SOCKET_CONTEXT_ARR[num]->m_username, u); num++; } //通IO操作一样,如果一个SOCKET结构体数组使用完毕,需要退出数组,那么直接关闭相应的Socket就可以了 void RemoveContext(PER_SOCKET_CONTEXT* S) { for (int i = 0; i < num; i++) { if (SOCKET_CONTEXT_ARR[i] == S) { //关闭相应的socket closesocket(SOCKET_CONTEXT_ARR[i]->m_Socket); num--; break; } } }};
结构体以及相应的方法便定义完成。
2.一些相关申明
#include "stdafx.h"#include "stdio.h"#include "winsock2.h" #include "ws2tcpip.h" #include "mswsock.h"#pragma comment(lib,"ws2_32.lib") #pragma warning(disable: 4996)#define MAX_BUFFER_LEN 4096#define MAX_POST_ACCEPT 6HANDLE mIoCompletionPort; //完成端口接收对象PER_IO_CONTEXT_ARR ArrayIoContext; //建立的io操作结构体对象PER_SOCKET_CONTEXT_ARR ArraySocketContext; //socket结构体结构体对象组DWORD WINAPI workThread(LPVOID lpParam); //申明线程PER_SOCKET_CONTEXT * ListenContext; //创建监听socketLPFN_ACCEPTEX mAcceptEx; //AcceptEx函数指针GUID GuidAcceptEx = WSAID_ACCEPTEX; //指针的GUID,这个是识别AcceptEx函数必须的LPFN_GETACCEPTEXSOCKADDRS mAcceptExSockAddrs; //AcceptEx指针GUID GuidGetAcceptExSockAddrs = WSAID_GETACCEPTEXSOCKADDRS; //同上 GUIDbool _PostSend(PER_IO_CONTEXT * pIoContext);bool _PostRecv(PER_IO_CONTEXT * pIoContext);//申明接收连接操作bool _PostAccept(PER_IO_CONTEXT * pAcceptContext);
3.main
在主线程中,不用做太多的事情,主要把acceptex这行东西搞定就行了,有啥事都扔给工作线程,让他去做。
int main(){ //绑定WSA WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != NO_ERROR) { printf("初始化失败!=%d \n", GetLastError()); return 1; } //建立完成端口 mIoCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); if (mIoCompletionPort == NULL) { printf("建立完成端口失败! =%d \n",WSAGetLastError()); return 2; } //创建线程 SYSTEM_INFO si; GetSystemInfo(&si); int m_nThreads = si.dwNumberOfProcessors * 2 + 2; //初始化线程句柄 HANDLE * m_phWorkThreads = new HANDLE[m_nThreads]; for (int i = 0; i < m_nThreads; i++) { m_phWorkThreads[i] = CreateThread(0,0,workThread,NULL,0,NULL); } printf("线程创立成功!\n"); //服务器地址 struct sockaddr_in ServerAddress; //socke ListenContext = new PER_SOCKET_CONTEXT; //通过WSASocket来建立Socekt,这样才能将socket和IO重叠结构绑定 ListenContext->m_Socket = WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED); if (ListenContext->m_Socket == INVALID_SOCKET) { printf("初始化SOCKET失败! =%d \n",WSAGetLastError()); return 3; } //填充服务器地址信息 ZeroMemory(&ServerAddress,sizeof(ServerAddress)); ServerAddress.sin_addr.S_un.S_addr = htonl(INADDR_ANY); ServerAddress.sin_family = AF_INET; ServerAddress.sin_port = htons(8000); //绑定地址和端口 if (bind(ListenContext->m_Socket,(SOCKADDR*)&ServerAddress,sizeof(ServerAddress))==SOCKET_ERROR) { printf("绑定端口失败! =%d",GetLastError()); return 4; } //开始对端口进行监听 if (listen(ListenContext->m_Socket, SOMAXCONN) == SOCKET_ERROR) { printf("监听失败! =%d",GetLastError()); return 5; } //将监听socket放入完成端口中 if ((CreateIoCompletionPort((HANDLE)ListenContext->m_Socket, mIoCompletionPort, (DWORD)ListenContext, 0) == NULL)) { printf("将服务端ListenContext结构体放入完成端口失败! =%d",GetLastError()); if (ListenContext->m_Socket == INVALID_SOCKET) { closesocket(ListenContext->m_Socket); ListenContext->m_Socket = INVALID_SOCKET; } return 3; } //使用AcceptEx指针,博文上说这个指针时微软的,直接调用就好了,在和其他人交流后知道,通过这个指针,以及相应的函数操作,可以很方便的将客户端的各种信息以及对应的socke传入到其他的方法中 DWORD dwBytes = 0; if (SOCKET_ERROR == WSAIoctl( ListenContext->m_Socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx), &mAcceptEx, sizeof(mAcceptEx), &dwBytes, NULL, NULL)) { printf("WSAIoctl 未能获取AcceptEx函数指针。= %d\n", WSAGetLastError()); return 6; } //同理getAccetExSocket那个指针一样的进行操作,两个指针一起用就可以达到很好的调用效果 if (SOCKET_ERROR == WSAIoctl( ListenContext->m_Socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidGetAcceptExSockAddrs, sizeof(GuidGetAcceptExSockAddrs), &mAcceptExSockAddrs, sizeof(mAcceptExSockAddrs), &dwBytes, NULL, NULL)) { printf("WSAIoctl 未能获取GuidGetAcceptExSockAddrs函数指针。= %d\n", WSAGetLastError()); return 7; } //投递accept io请求 for (size_t i = 0; i < MAX_POST_ACCEPT; i++) { //通过结构体对象中的新建IO操作方法绑定一个Accept PER_IO_CONTEXT * newAcceptIoContext = ArrayIoContext.GetNewIoContext(); //既然绑定了,那就判断一下连接接收操作 //如果连接都失败了,那就不用进行其他操作了,直接将之从ArrayIoContext队列中删除就行了。 if (_PostAccept(newAcceptIoContext) == false) { ArrayIoContext.RemoveContext(newAcceptIoContext); return false; } } //printf("投递了%d个Accept请求 \n",MAX_POST_ACCEPT); printf("服务初始化完成……\n等待客户端……\n"); //退出指令 //默认状态是正常运行 bool run = true; while (true) { char string[20]; gets_s(string); //如果输入字符串等于“exit”,返回值为false if (!strcmp("exit",string)) { run = false; } } WSACleanup(); return 0;}
4.WorkThread
DWORD WINAPI workThread(LPVOID lpParam){ //每个socket对应的重叠结构 OVERLAPPED * pOverlapped= NULL; //新的socket结构体 PER_SOCKET_CONTEXT *pListenContext = NULL; //接收的字符串 DWORD dwBytesTransfered = 0; //这里通过循环来处理客户端发送的请求 while (true) { //我们先需要去询问队列状态 BOOL bReturn = GetQueuedCompletionStatus( mIoCompletionPort, //建立的那个完成端口 &dwBytesTransfered, //操作完成后返回的字节数 (PULONG_PTR)&pListenContext, //绑定的那个结构体参数 &pOverlapped, //连接进来的时候建立的那个重叠结构体 INFINITE); //设置为一直等待 //前面我们投递AcceptEx参数的时候,就投递了一个重叠结构体,这个重叠结构体参数里面包含了我们需要的数据 //取出数据 PER_IO_CONTEXT * pIoContext = CONTAINING_RECORD(pOverlapped,PER_IO_CONTEXT,m_Overlapped); //判断客户端是否断开 if (!bReturn) { DWORD dwErr = GetLastError(); if (dwErr == 64) { printf("有客户端异常退出!\n"); } else { printf("客户端异常! %d\n",dwErr); } continue; } else { switch (pIoContext->m_OpType) { case ACCEPT: { //1.如果得到IO操作结构体中的状态符号为ACCEPT,那么久应该取出相对应的客户端的地址信息,做登录比对 SOCKADDR_IN *ClientAddr = NULL, *LocalAddr = NULL; int Len = sizeof(SOCKADDR_IN); mAcceptExSockAddrs( pIoContext->m_wsaBuf.buf, pIoContext->m_wsaBuf.len-((sizeof(SOCKADDR)+16)*2), sizeof(SOCKADDR) + 16, sizeof(SOCKADDR) + 16, (LPSOCKADDR*)&LocalAddr, &Len, (LPSOCKADDR*)&ClientAddr, &Len); printf("客户端 %s:%d 连入.\n", inet_ntoa(ClientAddr->sin_addr), ntohs(ClientAddr->sin_port)); //接收的用户名 char *input_username = new char[40]; //接收的密码 char *input_password = new char[40]; input_username = strtok(pIoContext->m_wsaBuf.buf, "#"); input_password = strtok(NULL, ""); char *user = new char[40]; strcpy(user, input_username); //是否登陆成功 bool enter = false; if (input_username != NULL && input_password != NULL) { //查找账号是否存在 for (int i = 0; i < sizeof(username) / sizeof(username[0]); i++) { int j = 0; for (j = 0; username[i][j] == input_username[j] && input_username[j]; j++); if (username[i][j] == input_username[j] && input_username[j] == 0) { //账号存在查找密码是否正确 int k; for (k = 0; password[i][k] == input_password[k] && input_password[k]; k++); if (password[i][k] == input_password[k] && input_password[k] == 0) { enter = true; } break; } } } if (enter) { printf("客户端 %s:%d 登陆成功! \n", inet_ntoa(ClientAddr->sin_addr), ntohs(ClientAddr->sin_port)); strcpy(pIoContext->m_wsaBuf.buf, "登陆成功!\n"); } else { printf("客户端 %s:%d 登陆失败!\n", inet_ntoa(ClientAddr->sin_addr), ntohs(ClientAddr->sin_port)); strcpy(pIoContext->m_wsaBuf.buf, "登陆失败!\n"); } //客户端登录成功后,把客户端的信息填入到相应的数组中(GetNewSocketContext) PER_SOCKET_CONTEXT* newSocketContext = ArraySocketContext.GetNewSocketContext(ClientAddr, user); newSocketContext->m_Socket = pIoContext->m_SocketAccept; memcpy(&(newSocketContext->m_ClientAddr), ClientAddr, sizeof(SOCKADDR_IN)); //绑定完成端口 HANDLE hTemp = CreateIoCompletionPort((HANDLE)newSocketContext->m_Socket, mIoCompletionPort, (DWORD)newSocketContext, 0); if (hTemp == NULL) { printf("创建完成端口失败! =%d \n",GetLastError()); break; } //给客户端SocketContext绑定一个Recv PER_IO_CONTEXT* pNewSendIoContext = ArrayIoContext.GetNewIoContext(); memcpy(&(pNewSendIoContext->m_wsaBuf.buf), &pIoContext->m_wsaBuf.buf, sizeof(pIoContext->m_wsaBuf.len)); pNewSendIoContext->m_SocketAccept = newSocketContext->m_Socket; //send出去 _PostSend(pNewSendIoContext); //查看是否登录成功 if (enter) { //给这个新建的socket结构体定一个PostRecv PER_IO_CONTEXT* pNewRecvIoContext = ArrayIoContext.GetNewIoContext(); pNewRecvIoContext->m_SocketAccept = newSocketContext->m_Socket; if (!_PostRecv(pNewRecvIoContext)) { ArrayIoContext.RemoveContext(pNewRecvIoContext); } } //给这个服务端的SocketContext绑定Accept pIoContext->ResetBuffer(); _PostAccept(pIoContext); } break; case RECV: { //执行recv后,进行接收数据的处理,发给别的客户端,并再recv if (dwBytesTransfered > 1) { char *Senddata = new char[MAX_BUFFER_LEN]; ZeroMemory(Senddata, MAX_BUFFER_LEN); char *temp = new char[MAX_BUFFER_LEN]; ZeroMemory(temp, MAX_BUFFER_LEN); char *sendname = new char[40]; ZeroMemory(sendname, 40); printf_s("客户端 %s(%s:%d) 发送:%s\n", pListenContext->m_username, inet_ntoa(pListenContext->m_ClientAddr.sin_addr), ntohs(pListenContext->m_ClientAddr.sin_port), pIoContext->m_szBuffer); sprintf_s(Senddata, MAX_BUFFER_LEN, "%s(%s:%d)说:%s\n", pListenContext->m_username, inet_ntoa(pListenContext->m_ClientAddr.sin_addr), ntohs(pListenContext->m_ClientAddr.sin_port), pIoContext->m_szBuffer); for (int i = 0; i < ArraySocketContext.num; i++) { PER_SOCKET_CONTEXT* cSocketContext = ArraySocketContext.getARR(i); if (cSocketContext->m_Socket == pListenContext->m_Socket) { continue; } //判断是否不是单对单消息,且消息有长度 if (strlen(sendname) == 0 && strlen(Senddata) > 0) { // 给这个客户端SocketContext绑定一个Recv的计划 PER_IO_CONTEXT* pNewSendIoContext = ArrayIoContext.GetNewIoContext(); memcpy(&(pNewSendIoContext->m_wsaBuf.buf), &Senddata, sizeof(Senddata)); pNewSendIoContext->m_SocketAccept = cSocketContext->m_Socket; // Send投递出去 _PostSend(pNewSendIoContext); } } } pIoContext->ResetBuffer(); _PostRecv(pIoContext); } break; case SEND: ArrayIoContext.RemoveContext(pIoContext); break; default: printf("_WorkThread中的 pIoContext->m_OpType 参数异常.\n"); break; } } } printf("线程退出!"); return 0;}
5.相关操作
//投递发送数据请求bool _PostSend(PER_IO_CONTEXT * pIoContext){ //初始化变量 DWORD dwFlags = 0; DWORD dwBytes = 0; pIoContext->m_OpType = SEND; WSABUF *p_wbuf = &pIoContext->m_wsaBuf; OVERLAPPED *p_ol = &pIoContext->m_Overlapped; pIoContext->ResetBuffer(); if ((WSASend(pIoContext->m_SocketAccept, p_wbuf, 1, &dwBytes, dwFlags, p_ol, NULL) == SOCKET_ERROR) && (WSAGetLastError() != WSA_IO_PENDING)) { ArrayIoContext.RemoveContext(pIoContext); return false; } return true;}//投递接收数据请求bool _PostRecv(PER_IO_CONTEXT * pIoContext){ // 初始化变量 DWORD dwFlags = 0; DWORD dwBytes = 0; pIoContext->m_OpType = RECV; WSABUF *p_wbuf = &pIoContext->m_wsaBuf; OVERLAPPED *p_ol = &pIoContext->m_Overlapped; pIoContext->ResetBuffer(); int nBytesRecv = WSARecv(pIoContext->m_SocketAccept, p_wbuf, 1, &dwBytes, &dwFlags, p_ol, NULL); // 如果返回值错误,并且错误的代码并非是Pending的话,那就说明这个重叠请求失败了 if (nBytesRecv == SOCKET_ERROR && (WSAGetLastError() != WSA_IO_PENDING)) { if (WSAGetLastError() != 10054) { printf("投递一个WSARecv失败!%d \n", WSAGetLastError()); } return false; } return true;}//投递操作请求bool _PostAccept(PER_IO_CONTEXT * pAcceptContext){ //投递Accept请求,准备参数 DWORD dwBytes = 0; pAcceptContext->m_OpType = ACCEPT; WSABUF *p_wbuf = &pAcceptContext->m_wsaBuf; OVERLAPPED *p_ol = &pAcceptContext->m_Overlapped; //为以后新连接进来的客户端准备好socket pAcceptContext->m_SocketAccept = WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,WSA_FLAG_OVERLAPPED); if (pAcceptContext->m_SocketAccept == INVALID_SOCKET) { printf("新建socket失败! =%d \n", WSAGetLastError()); return false; } //投递AcceptEx参数 if (mAcceptEx(ListenContext->m_Socket, pAcceptContext->m_SocketAccept, p_wbuf->buf, p_wbuf->len - ((sizeof(SOCKADDR_IN) + 16) * 2), sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &dwBytes, p_ol) == FALSE) { if (WSAGetLastError() != ERROR_IO_PENDING) { printf("投递Accept操作失败! =%d\n",WSAGetLastError()); return false; } } return true;}
大致代码就是这样的,就思路上是可以实现我的聊天目的,但是就代码编程来说,很冗余和多此一举的。
提醒下自己,以后有所提升就回来重新修改我的第一份C++小程序!
阅读全文
0 0
- 完成端口学习之简易聊天室
- Websocket学习--简易聊天室
- 基于完成端口的聊天室系统
- PHP菜鸟之简易聊天室
- asp.net之简易聊天室
- socket通信之九:使用完成端口实现的一个聊天室
- 简易的TCP完成端口库
- Qt学习之路-简易画板5(完成)
- 完成端口学习笔记
- 完成端口模型学习
- 完成端口学习
- nio学习实践--简易的群体聊天室
- 简易聊天室
- Android学习笔记(31) --- 网络通信之Socket简易聊天室
- IO完成端口学习示例
- IO完成端口学习示例
- 完成端口之个人理解
- win32线程之完成端口
- mysql行转列、列转行
- 使用 NW.js 跨平台开发
- 使用AV Pro Video 在unity里播放视频最简单操作
- 关于supervisor安装及配置
- SV之OOP基础知识
- 完成端口学习之简易聊天室
- FreeRTOS代码剖析之2:内存管理Heap_2.c
- nginx常用操作命令
- 安装libpng-1.6.10时make出现错误,请帮忙
- springMVC图片上传
- Android framework 的理解
- nodejs 使用assert做参数验证
- 编写jQuery插件
- 使用pm2把项目发布到服务器上