zoukankan      html  css  js  c++  java
  • 串口通讯的代码 。是别人写的 我加了些注释。

    // Communication.h: interface for the CCommunication class.
    //
    //////////////////////////////////////////////////////////////////////
    
    #if !defined(AFX_COMMUNICATION_H__6CA00576_F088_11D1_89BB_8311A0F2733D__INCLUDED_)
    #define AFX_COMMUNICATION_H__6CA00576_F088_11D1_89BB_8311A0F2733D__INCLUDED_
    
    #if _MSC_VER >= 1000
    #pragma once
    #endif // _MSC_VER >= 1000
    /*
    _MSC_VER 定义编译器的版本
    MS VC++ 10.0 _MSC_VER = 1600 
    MS VC++ 9.0 _MSC_VER = 1500 
    MS VC++ 8.0 _MSC_VER = 1400 
    MS VC++ 7.1 _MSC_VER = 1310 
    MS VC++ 7.0 _MSC_VER = 1300 
    MS VC++ 6.0 _MSC_VER = 1200 
    MS VC++ 5.0 _MSC_VER = 1100 
    其中MS VC++ 10.0就是Visual C++ 2010,MS VC++ 9.0就是Visual C++ 2008,MS VC++ 8.0就是Visual C++ 2005。
    
    在程序中加入_MSC_VER宏可以根据编译器版本让编译器选择性地编译一段程序。
    例如一个版本编译器产生的lib文件可能不能被另一个版本的编译器调用,
    那么在开发应用程序的时候,在该程序的lib调用库中放入多个版本编译器产生的lib文件。
    在程序中加入_MSC_VER宏,编译器就能够在调用的时根据其版本自动选择可以链接的lib库版本,如下所示。 
    #if _MSC_VER >= 1400 // for vc8, or vc9 
    #ifdef _DEBUG 
    #pragma comment( lib, "SomeLib-vc8-d.lib" ) 
    #else if 
    #pragma comment( lib, "SomeLib-vc8-r.lib" ) 
    #endif 
    #else if _MSC_VER >= 1310 // for vc71 
    #ifdef _DEBUG 
    #pragma comment( lib, "SomeLib-vc71-d.lib" ) 
    #else if 
    #pragma comment( lib, "SomeLib-vc71-r.lib" ) 
    #endif 
    #else if _MSC_VER >=1200 // for vc6 
    #ifdef _DEBUG 
    #pragma comment( lib, "SomeLib-vc6-d.lib" ) 
    #else if 
    #pragma comment( lib, "SomeLib-vc6-r.lib" ) 
    #endif 
    #endif 
    
    */
    
    #define WM_RECEIVEPACKET WM_USER+100
    
    class CCommunication  
    {
    public:
    //	BOOL Connect;
    	//发送数据函数
    	int SendData(char *data,int len);
    
    	//设定消息接收者
    	void SetMessageReceiver(CWnd *pWnd);
    
    	//初始化函数
    	BOOL Initialize(char *device,DWORD BaudRate,int Bits,int DDV,int StopBit);
    
    	//构造函数
    	CCommunication();
    	//析构函数
    	virtual ~CCommunication();
    
    	//关闭通讯接口
    	BOOL CloseSerialPort();
    
    	//CWnd是MFC窗口类的基类,提供了微软基础类库中所有窗口类的基本功能
    	CWnd *msg_receiver;
    
    	//OVERLAPPED是一个包含了用于异步输入输出的信息的结构体
    	OVERLAPPED write_os;
    
    	//定义一个句柄
    	HANDLE hComPort;
    
    private:
    
    };
    
    #endif // !defined(AFX_COMMUNICATION_H__6CA00576_F088_11D1_89BB_8311A0F2733D__INCLUDED_)
    /*
    第一种声明:
    
    typedef struct _OVERLAPPED { 
      DWORD Internal; 
      DWORD InternalHigh; 
      DWORD Offset; 
      DWORD OffsetHigh; 
      HANDLE hEvent; 
      } OVERLAPPED
    参数说明:
    
    Internal: 预留给操作系统使用。
    它指定一个独立于系统的状态,当GetOverlappedResult函数返回时没有设置扩展错误信息ERROR_IO_PENDING时有效。
    
    InternalHigh: 预留给操作系统使用。它指定长度的数据转移,当GetOverlappedResult函数返回TRUE时有效。
    
    Offset: 该文件的位置是从文件起始处的字节偏移量。调用进程设置这个成员之前调用ReadFile或WriteFile函数。
    当读取或写入命名管道和通信设备时这个成员被忽略设为零。
    
    OffsetHigh: 指定文件传送的字节偏移量的高位字。当读取或写入命名管道和通信设备时这个成员被忽略设为零。
    
    hEvent: 在转移完成时处理一个事件设置为有信号状态。
    调用进程集这个成员在调用ReadFile、 WriteFile、TransactNamedPipe、 ConnectNamedPipe函数之前。
    */
    
    /*
    第二种声明:
    
    typedef struct _OVERLAPPED {
    ULONG_PTR Internal; //操作系统保留,指出一个和系统相关的状态
    ULONG_PTR InternalHigh; //指出发送或接收的数据长度
    union {
    struct {
    DWORD Offset; //文件传送的字节偏移量的低位字
    DWORD OffsetHigh; //文件传送的字节偏移量的高位字
    };
    PVOID Pointer; //指针,指向文件传送位置
    };
    HANDLE hEvent; //指定一个I/O操作完成后触发的事件
    } OVERLAPPED, *LPOVERLAPPED;
    */
    
    /*
    I/O设备处理必然让主程序停下来干等I/O的完成,解决这个问题,可以使用OVERLAPPED。
    OVERLAPPED I/O是WIN32的一项技术, 你可以要求操作系统为你传送数据,并且在传送完毕时通知你。
    这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。
    事实上,操作系统内部正是以线程来I/O完成OVERLAPPED I/O。
    ,而不需付出什么痛苦的代价。也就是说,OVERLAPPED主要是设置异步I/O操作,
    异步I/O操作是指应用程序可以在后台读或者写数据,而在前台做其他事情。
    */
    

      

    // Communication.cpp: implementation of the CCommunication class.
    //
    //////////////////////////////////////////////////////////////////////
    
    #include "stdafx.h"
    #include "Communication.h"
    
    #ifdef _DEBUG
    #undef THIS_FILE
    static char THIS_FILE[]=__FILE__;
    #define new DEBUG_NEW
    #endif
    
    //////////////////////////////////////////////////////////////////////
    // Construction/Destruction
    //////////////////////////////////////////////////////////////////////
    BOOL CState;//定义一个全局的变量 CState。
    
    //构造函数  用于给全局变量CState置ON
    CCommunication::CCommunication()
    {
    	CState=TRUE;
    }
    //析构函数  用于给全局变量CState置OFF
    CCommunication::~CCommunication()
    {
       CState=FALSE;
    }
    
    
    //具体的函数定义  。这个CommWatchProc 这个函数 一个线程函数, 这个函数是为了处理 串口相关事件的处理。
    //在后面的代码里面,有个AfxCreateThrend的函数 是用于创建线程的,其中第一个参数 就要填CommWatchProc
    UINT CommWatchProc(LPVOID lpData)
    {
    //Add message WM_RECEIVEPACKET handler in class caller:
    //(wParam=pointer to data block   lParam=length of data block
    
    //Add message WM_SENDSUCCESS handler in class caller
    	CCommunication* com=(CCommunication*)lpData;
       DWORD       dwEvtMask ;
       OVERLAPPED  os;
       COMSTAT comstat;
       DWORD dwErrorFlag;
       DWORD dwLength;
    	//		AfxMessageBox("Receiving");
    
       memset( &os, 0, sizeof( OVERLAPPED ) ) ;
    
       // create I/O event used for overlapped read
    
       os.hEvent = CreateEvent( NULL,    // no security
                                TRUE,    // explicit reset req
                                FALSE,   // initial event reset
                                NULL ) ; // no name
       if (os.hEvent == NULL)
       {
          MessageBox( NULL, _T("Failed to create event for thread!"),
    		          _T("Communication Error!"),
                      MB_ICONEXCLAMATION | MB_OK ) ;
          return ( FALSE ) ;
       }
    
       if (!SetCommMask(com->hComPort, EV_RXCHAR ))
          return ( FALSE ) ;
       DWORD dwRead;
       char *buf;
       buf=new char[MAX_PATH];
       memset(buf,0,MAX_PATH);
       while (CState)
       {
    		dwEvtMask = 0 ;
    
    		WaitCommEvent(com->hComPort, &dwEvtMask, 0 );//等待串口通讯的事件的发生。
    		Sleep(100);
    		//memset(buf,0,MAX_PATH);检测返回的dwEvtMask,知道发生了什么样的串口事件
    		if ((dwEvtMask & EV_RXCHAR) == EV_RXCHAR)
    		{//EV_RXCHAR代表缓冲区 有数据过来了
    			//read code
    			ClearCommError(com->hComPort,&dwErrorFlag,&comstat);//清除错误
    			dwLength=comstat.cbInQue;//输入缓冲区有多少数据?	
    			if (dwLength)//如果大于0
    			{
    				
    				if (ReadFile(com->hComPort,buf,dwLength,&dwRead,&os))//调用ReadFile函数读取缓冲区数据	
    				{
    					com->msg_receiver->PostMessage(WM_RECEIVEPACKET,(WPARAM)buf,(LPARAM)dwRead);	
    					//想系统消息队列放入一个消息,通知程序的主线程,串口受到了数据。
    				}
    			}
    		}
       }
       delete buf;
       // get rid of event handle
       CloseHandle( os.hEvent ) ;
       return( TRUE ) ;
    } // end of CommWatchProc()
    
    //初始化端口  定义端口的带宽 位数 奇偶校验 停止位之类的
    BOOL CCommunication::Initialize(char * device,DWORD BaudRate,int Bits,int DDV,int StopBit)
    {
    //   srand( (unsigned)time( NULL ) );
       // open COMM device
    	CState=TRUE;
    	//将函数CreateFileA的的返回值赋值给hComPort,并且判定其等于(HANDLE) -1
       if (
    	   (hComPort =
          CreateFileA( device,//设备名称  可以是COM2 COM3 之类的名字
    					GENERIC_READ | GENERIC_WRITE,//允许读写
                      0,                    // exclusive access  必须是0
                      NULL,                 // no security attrs
                      OPEN_EXISTING,		//设置产生方式
                      FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // overlapped I/O  使用异步通讯
                      NULL )
    				  
    				  ) == (HANDLE) -1
    		)
          return ( FALSE ) ;
       // get any early notifications
    
       SetCommMask(hComPort, EV_RXCHAR ) ;//设置事件驱动类型
    
       // setup device buffers
    
       SetupComm(hComPort, 10240, 10240 ) ;//设置输入 输出缓冲区的大小
    
       // purge any information in the buffer
    
       PurgeComm(hComPort, PURGE_TXABORT | PURGE_RXABORT |
                                       PURGE_TXCLEAR | PURGE_RXCLEAR ) ;//清干净输入和输出缓冲区
    
       // set up for overlapped I/O
       COMMTIMEOUTS CommTimeOuts;	//定义超时结构, 并且给结构的中的值赋值  
       CommTimeOuts.ReadIntervalTimeout = 0xFFFFFFFF ;
       CommTimeOuts.ReadTotalTimeoutMultiplier = 0 ;
       CommTimeOuts.ReadTotalTimeoutConstant = 1000 ;
       CommTimeOuts.WriteTotalTimeoutMultiplier = 0 ;
       CommTimeOuts.WriteTotalTimeoutConstant = 10000 ;
       SetCommTimeouts(hComPort, &CommTimeOuts ) ;
    
    
    
       DCB dcb;//定义数据控制块结构 
       dcb.DCBlength=sizeof(DCB);
       dcb.BaudRate=BaudRate;//通讯波特率
       dcb.fBinary=1;
       dcb.fParity=1;
       dcb.fOutxCtsFlow=0;
       dcb.fOutxDsrFlow=0;
       dcb.fDtrControl=0;
       dcb.fDsrSensitivity=0;
       dcb.fTXContinueOnXoff=0;
       dcb.fOutX=0;
       dcb.fInX=0;
       dcb.fErrorChar=0;
       dcb.fNull=0;
       dcb.fRtsControl=0;
       dcb.fAbortOnError=0;
    //   dcb.wReserved=0;
       dcb.XonLim=0;
       dcb.XoffLim=0;
       dcb.ByteSize=Bits;//数据位长度  Bits 就是八位的意思  也可以写数字8
       dcb.Parity=0;
       dcb.StopBits=ONESTOPBIT;//停止位 是几位  此处写的一位
       //dcb.StopBits=StopBit;
    
       if (!SetCommState(hComPort,&dcb))//数据配置完毕以后,就调用SetCommState函数 给端口配置
       return (FALSE);//如果配置不成功就返回 FALSE
    
       if (!AfxBeginThread(CommWatchProc,(LPVOID)this))//启动一个辅助线程,用于串口事件的处理
    	   //此处需要对线程说明一下:
    	   /*
    	   Windows提供了两种线程,辅助线程和用户界面线程。区别在于:辅助线程
    没有窗口,所以它没有自己的消息循环。但是辅助线程很容易编程,通常也
    很有用。我们使用辅助线程。主要用它来监视串口状态,看有无数据到达、通
    信有无错误;而主线程则可专心进行数据处理、提供友好的用户界面等重要
    的工作。
    辅助线程还有一个名字 就是工作线程。
    	   */
       {
        CloseHandle(hComPort) ;//如果线程没有启动成功,就关闭句柄,并且返回FAlSE
        return (FALSE);
       }
       else
       {
        // assert DTR and RTS
        EscapeCommFunction(hComPort,SETDTR);
        EscapeCommFunction(hComPort,SETRTS);
       }
       memset( &write_os, 0, sizeof( OVERLAPPED )) ;
       write_os.hEvent = CreateEvent( NULL,    // no security
                                      TRUE,    // explicit reset req
                                      FALSE,   // initial event reset
                                      NULL ); // no name
       if (NULL == write_os.hEvent)
       {
          CloseHandle( write_os.hEvent ) ;
          return (FALSE) ;
       }
       return(TRUE);
    }
    
    //关闭串口的端口
    BOOL CCommunication::CloseSerialPort()
    {
    	CState=FALSE;
    	Sleep(10);
    	if(!CloseHandle(hComPort)) return FALSE;
    	else return TRUE;
    }
    
    
    void CCommunication::SetMessageReceiver(CWnd * pWnd)
    {
    	//Add a line in class caller:
    	//??.SetMessageReceiver(this);
    	msg_receiver=pWnd;
    }
    
    //函数: 用于发送数据 。需要提交的是发送数据的字符内容 和长度  
    int CCommunication::SendData(char * data, int len)
    {
    	CString b;
    	int a=1,c=1;
    	unsigned long sendlen;
        a=WriteFile(hComPort, data, len, &sendlen, &write_os);//其实其内部调用的是WriteFile这个函数。
    	//从本质上  系统把通讯的东西看成一个文件的读写。
    	CString strText;
    	strText.Format(",,'%s",data);
    //	::Log(GetDirectory()+"\Log\Com.csv",strText.Left(31),TRUE,"Time,Receive,send");
    	return sendlen;
    }
    
    /*
    使用多线程技术,在工作线程(也就是所谓的辅助线程)里面监视串口,有数据到达时依靠事件驱动,
    读取数据后向主线程汇报,注意发送数据的工作在主线程里面完成,因为通常来说发送数据的
    内容(也就是所谓的下行数据)数量比较少,并且WaitCommEvent,ReadFile(),WriteFile()	都
    使用非阻塞通信技术。依靠重叠(Overlappend)读写操作,让串口的读写工作在后台完成。
    */
    
    /*
    读写文件是每个Windows软件开发人员都需要做的工作。可见这项工作是非常重要的,
    毕竟各种各样的数据都需要保存起来,以便作各种各样的分析,或者通过网络传送给别人。
    像大家用BT下载的电影,在那个BT软件里,就需要不断从网络里接收到数据,
    然后再把这些数据保存到文件里合适的位置,就可以生成跟发行者那里一样的文件,
    这样才可以播放出来。又比如我在玩《征途》的游戏里,刚刚打开游戏时,
    它就不断从服务器上下载更新的文件下来,然后保存到硬盘。WriteFile函数是用来写数据到文件,
    ReadFile函数是从文件里读取数据出来。但这两个函数不但可以读取写磁盘的文件,
    也可以接收和发送网络的数据,还有读写串口、USB、并口等设备的数据。在读写文件里,
    首先就是先打开文件,然后判断打开是否成功。在写文件时,同时要注意磁盘的空间是否满等问题。
    在读取文件时,往往需要读取不同位置的文件,比如要读取一个4G的视频文件,
    就不可能完全把它读取到内存里,因此就需要对文件进行定位读取。
    */
    
    
    /*
    函数WriteFile和ReadFile声明如下:
    WINBASEAPI
    BOOL
    WINAPI
    WriteFile(
        __in        HANDLE hFile,//是文件句柄
        __in_bcount(nNumberOfBytesToWrite) LPCVOID lpBuffer,//是读写数据缓冲区
        __in        DWORD nNumberOfBytesToWrite,//是多少数据要写入
        __out_opt   LPDWORD lpNumberOfBytesWritten,//是已经写入多少数据
        __inout_opt LPOVERLAPPED lpOverlapped//是异步读写的结构
        );
       
    WINBASEAPI
    BOOL
    WINAPI
    ReadFile(
        __in        HANDLE hFile,//是文件句柄
        __out_bcount_part(nNumberOfBytesToRead, *lpNumberOfBytesRead) LPVOID lpBuffer,//是读写数据缓冲区
        __in        DWORD nNumberOfBytesToRead,//是多少数据要读取
        __out_opt   LPDWORD lpNumberOfBytesRead,//是已经读取多少数据
        __inout_opt LPOVERLAPPED lpOverlapped//是异步读写的结构
        );
     
    hFile是文件句柄。
    lpBuffer是读写数据缓冲区。
    nNumberOfBytesToWrite是多少数据要写入。
    lpNumberOfBytesWritten是已经写入多少数据。
    nNumberOfBytesToRead是多少数据要读取。
    nNumberOfBytesToRead是已经读取多少数据。
    lpOverlapped是异步读写的结构。
     
     
    调用函数的例子如下:
    #001  //创建、写入、读取文件。
    #002  //蔡军生 2007/10/21 QQ:9073204 深圳
    #003  void CreateFileDemo(void)
    #004  {
    #005         //
    #006         HANDLE hFile = ::CreateFile(_T("CreateFileDemo.txt"),     //创建文件的名称。
    #007               GENERIC_WRITE|GENERIC_READ,          // 写和读文件。
    #008               0,                      // 不共享读写。
    #009               NULL,                   // 缺省安全属性。
    #010               CREATE_ALWAYS,          // 如果文件存在,也创建。
    #011               FILE_ATTRIBUTE_NORMAL, // 一般的文件。      
    #012               NULL);                 // 模板文件为空。
    #013 
    #014         if (hFile == INVALID_HANDLE_VALUE)//根据返回文件句柄的值,判定创建是否成功。
    #015         {
    #016               //
    #017               OutputDebugString(_T("CreateFile fail!/r/n"));
    #018         }
    #019 
    
    #020         //往文件里写数据。
    #021         const int BUFSIZE = 4096;//定义缓冲区的大小
    #022         char chBuffer[BUFSIZE];    //定义一个缓冲区 其实就是一个字符数组     
    #023         memcpy(chBuffer,"Test",4);//由src指向地址为起始地址的连续n个字节的数据复制到
                                           //以destin指向地址为起始地址的空间内。
    #024         DWORD dwWritenSize = 0;
    #025        BOOL bRet = ::WriteFile(hFile,chBuffer,4,&dwWritenSize,NULL);//将字符数组里面
    									   //的数据写入 文件句柄对应的那块内存区域里面。
    #026         if (bRet)
    #027         {
    #028               //
    #029               OutputDebugString(_T("WriteFile 写文件成功/r/n"));
    #030         }
    #031 
    #032         //先把写文件缓冲区的数据强制写入磁盘。
    #033         FlushFileBuffers(hFile);
    #034 
    #035         //
    #036         //从文件里读取数据。
    #037         LONG lDistance = 0;
    #038         DWORD dwPtr = SetFilePointer(hFile, lDistance, NULL, FILE_BEGIN);
    #039         if (dwPtr == INVALID_SET_FILE_POINTER)
    #040         {
    #041               //获取出错码。
    #042               DWORD dwError = GetLastError() ;
    #043               //处理出错。           
    #044         }
    #045 
    #046         DWORD dwReadSize = 0;
    #047        bRet = ::ReadFile(hFile,chBuffer,4,&dwReadSize,NULL);
    #048         if (bRet)
    #049         {
    #050               //
    #051               OutputDebugString(_T("ReadFile 读文件成功/r/n"));
    #052         }
    #053         else
    #054         {
    #055               //获取出错码。
    #056               DWORD dwError = GetLastError();
    #057               //处理出错。           
    #058               TCHAR chErrorBuf[1024];
    #059               wsprintf(chErrorBuf,_T("GetLastError()=%d/r/n"),dwError);
    #060               OutputDebugString(chErrorBuf);
    #061         }
    #062 
    #063  }
    */
    
    
    /*
    在软件的需求里,把有用的数据保存起来是非常重要的功能。
    比如每天的股票行情数据需要保存起来,以便生成K线图。
    比如游戏客户端的LOG需要保存起,以便客户端出错时可以把LOG发送回来分析它出错的原因。
    比如银行每天进行交易时,也需要把所有交易的数据保存到文件备份起来,以便进行结算。
    还有在数据采集领域更是需要保存更多的数据,比如从DV里读取视频和语音数据出来,
    就会生成12G的巨型文件。比如读DVD光盘里,把光盘做成虚拟光驱也有9G大小。
    因此,创建文件是非常普通的功能,这个肯定是掌握,并且非常会使用的。
    当然这个CreateFile函数不但可以创建文件,还可以打串口、并口、网络、USB设备等功能。
      
    函数CreateFile声明如下:
    WINBASEAPI
    __out
    HANDLE
    WINAPI
    CreateFileA(
        __in     LPCSTR lpFileName,
        __in     DWORD dwDesiredAccess,
        __in     DWORD dwShareMode,
        __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
        __in     DWORD dwCreationDisposition,//是创建属性
        __in     DWORD dwFlagsAndAttributes,
        __in_opt HANDLE hTemplateFile
        );
    
    WINBASEAPI
    __out
    HANDLE
    WINAPI
    CreateFileW(
        __in     LPCWSTR lpFileName,//是文件或是设备的名称
        __in     DWORD dwDesiredAccess,//是访问属性  
        __in     DWORD dwShareMode,//是共享模式
        __in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,//是安全属性
        __in     DWORD dwCreationDisposition,//是创建属性
        __in     DWORD dwFlagsAndAttributes,//是文件标志和属性
        __in_opt HANDLE hTemplateFile//是文件模板
        );
    
    #ifdef UNICODE
    #define CreateFile CreateFileW
    #else
    #define CreateFile CreateFileA
    #endif // !UNICODE
    lpFileName是文件或设备的名称。
    dwDesiredAccess是访问属性。
    dwShareMode是共享属性。
    lpSecurityAttributes是安全属性。
    dwCreationDisposition是创建属性。
    dwFlagsAndAttributes是文件标志和属性。
    hTemplateFile是文件模板。
     
    调用函数的例子如下:
    #001  //创建文件。
    #002  //蔡军生 2007/10/18 QQ:9073204 深圳
    #003  void CreateFileDemo(void)
    #004  {
    #005         //
    #006         HANDLE hFile = ::CreateFile(_T("CreateFileDemo.txt"),     //创建文件的名称。
    #007              GENERIC_WRITE,          // 写文件。
    #008              0,                      // 不共享读写。
    #009              NULL,                   // 缺省安全属性。
    #010              CREATE_ALWAYS,          // 如果文件存在,也创建。
    #011              FILE_ATTRIBUTE_NORMAL, // 一般的文件。         
    #012              NULL);                 // 模板文件为空。
    #013 
    #014         if (hFile == INVALID_HANDLE_VALUE)
    #015         {
    #016               //
    #017               OutputDebugString(_T("CreateFile fail!/r/n"));
    #018         }
    #019  }
    */
    

      

  • 相关阅读:
    微信和支付宝支付模式详解及实现(.Net标准库)- OSS开源系列
    Linux+Nginx+Asp.net Core及守护进程部署
    Docker基础入门及示例
    this的指向问题
    H5C3-JS 此后面试暂不记录了 因为我发现了错题集,直接看就行了
    H5C3-JS day04
    H5C3-JS day03
    two-sum
    H5C3-JS day02
    三次握手四次挥手
  • 原文地址:https://www.cnblogs.com/wenluderen/p/4768697.html
Copyright © 2011-2022 走看看