文章目录

  • 一 TCP通信服务端和客户端——和UDP区别
    • (1)服务端
    • (2)客户端
  • 二:TCP通信-多进程/线程
    • (1)多进程版本
    • (2)多线程版本
    • (3)线程池版本

一 TCP通信服务端和客户端——和UDP区别

TCP是面向字节流的,是有连接的,会在服务端和客户端之间建立一条连接,而UDP显得就比较简单,只负责传递。在2-3:套接字(Socket)编程之UDP通信这一节详细叙述了UDP通信及套接字相关内容,本节TCP通信将会在上节的基础上,对TCP和UDP中代码的不同部分做以补充。

(1)服务端

tcpServer.h

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;#define  BACKLOG 5class tcpServer
{private:int _port;int listen_sock;
public:tcpServer(int port=8080):_port(port){}void initServer(){listen_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字创建失败"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"绑定失败"<<endl;exit(3);}//开启监听if(listen(listen_sock,BACKLOG) < 0){cout<<"绑定失败"<<endl;exit(4);} }//通信服务void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);//区别UDPif(s > 0){buffer[s]='\0';cout<<"服务端收到客户端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);//区别UDP}if(s==0)//客户端下线,服务端收到0{cout<<"客户端已经下线"<<endl;close(sock);//注意关闭套接字,资源是有限的break;}}}void startServer(){sockaddr_in endpoint;while(1){//TCP-acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失败"<<endl;continue;}cout<<"一台新的客户端已经连接"<<endl;//拿到套接字进行通信Service(_sock);}}~tcpServer(){close(listen_sock);}
};

1:创建套接字

相较于UDP,TCP通信时选择的套接字是流式套接字

listen_sock=socket(AF_INET,SOCK_STREAM,0);

2:listen监听
在套接字绑定之前,UDP和TCP基本是一致的。在TCP通信中,有两套套接字,其中一套用于监听,称之为监听套接字。TCP不同于UDP,如果和客户端之间没有连接就不能发送数据,所以要把一个套接字设置为监听状态,以便在任意时候客户端请求服务器时都能有套接字与该客户端建立连接。关于BACKLOG选项后序再网络原理里面再做解释

函数原型如下

 #include <sys/socket.h>int listen(int s, int backlog);

3:accept

accept表示服务端接受一个连接,每当一个客户端连接成功时,就会建立一条连接。最为关键的是该接口的返回值,其返回值也是一个套接字,前面说过TCP通信中存在两套套接字,一个就是刚才说到的过的监听套接字,它的职责就是不断从网络中获取连接,因为可能会用很多客户端想要和服务端通信,当它把连接拿上来之后,调用接口accept,其返回值所产生的套接字就是专门用来处理这条连接,用于进行通信的。这样做的话整个服务端只需要一个监听套接字,同时连接只要成功,只需调用接口产生新的套接字再用于通信即可。

 int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);

当accept失败时,该接口返回值也会小于0,需要注意的是此时只是一个连接失败了,你不能因为这么一个连接失败了,而把整个服务器给退了,所以要继续continue。
当accept成功时,我们建立一个新的函数,该函数就是用来专门去处理通信问题的

如下在accept成功之后,加入这样一句代码,表示连接上了一台新的客户端

然后使用telnet命令,如果你的Centos没有这个命令,需要进行一定配置,链接如下,亲测有效

CentOS 7.4安装telnet服务端

然后使用本地环回测试,使用telnet进行连接,可以发现当一台主机连接成功时,服务端提示出了相应的讯息

4:recv和send
不同于UDP中的recvfrom和sendto,在TCP通信中,我们尽可能采用的是recv和send来接受和发送

#include <sys/types.h>
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);
int send(int s, const void *msg, size_t len, int flags);

(2)客户端

tcpClient.h

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;class tcpClient
{private:string _ip;//服务端IP和端口号int _port;int _sock;
public:tcpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port){}void initClient(){_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDPif(_sock < 0){cout<<"套接字创建失败"<<endl;exit(2);}struct sockaddr_in sev;sev.sin_family=AF_INET;sev.sin_port=htons(_port);sev.sin_addr.s_addr=inet_addr(_ip.c_str());//TCP-connectif(connect(_sock,(struct sockaddr*)&sev,sizeof(sev))!=0){cout<<"connect失败"<<endl;exit(3);}}void startClient(){char mssage[64];while(1){   size_t s=read(0,mssage,sizeof(mssage)-1);//从标准输入读入if(s > 0){mssage[s-1]='\0';//剔除换行符send(_sock,mssage,strlen(mssage),0);ssize_t ret=recv(_sock,mssage,sizeof(mssage)-1,0);if(ret > 0){cout<<"客户端得到服务端消息"<<mssage<<endl;}}}}~tcpClient(){close(_sock);}};

1:connect

TCP是面向连接,因此对于客户端来说,它就要调用connect接口连接服务端,如果返回值为0表示连接成功

connect(_sock,(struct sockaddr*)&sev,sizeof(sev))

2:客户端退出

当客户端退出时,我们要让服务器知道客户端退出,并且关闭已经打开的套接字
那么服务端如何知道客户端退出了呢,这其实和recv接口的返回值有关

These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate theerror. The return value will be 0 when the peer has performed an orderly shutdown.

它的意思就说如果客户端下线,那么服务端将会接受到0 因此,服务端会有下面这样代码

if(s==0)
{cout<<"客户端已经下线"<<endl;close(sock);break;
}

二:TCP通信-多进程/线程

使用上面的代码,利用telnet进行测试,xshell中有多个窗口,代表多个客户端,第一个客户端连接后,服务端的确打印出了相关讯息,但是第二个和第三个在连接时却没有打印出信息

问题的原因就是咋们编写的服务器目前是一个单进程版的服务器,当第一个主机连接时,由于没有发送数据,因此进程会被阻塞在service函数中,到时后面的客户端连接不上

(1)多进程版本

tcpServer.h

#include <iostream>
#include <signal.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;#define  BACKLOG 5class tcpServer
{private:int _port;int listen_sock;
public:tcpServer(int port=8080):_port(port){}void initServer(){signal(SIGCHLD,SIG_IGN);//让子进程自己销毁listen_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字创建失败"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"绑定失败"<<endl;exit(3);}//开启监听if(listen(listen_sock,BACKLOG) < 0){cout<<"绑定失败"<<endl;exit(4);}}void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';cout<<"服务端收到客户端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);}if(s==0){cout<<"客户端已经下线"<<endl;close(sock);break;}}}void startServer(){sockaddr_in endpoint;while(1){//acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失败"<<endl;continue;}string client_info=inet_ntoa(endpoint.sin_addr);client_info+=":";client_info+=to_string(ntohs(endpoint.sin_port));cout<<"一台新的客户端已经连接:"<<client_info<<endl;pid_t id=fork();if(id==0)//子进程{close(listen_sock); //子进程会以父进程为模板,复制父进程PCB,所以对于子进程来说,可以关闭的它的listen_sock,尽量节省资源Service(_sock);exit(0);//子进程处理完毕}//对于父进程它只关心监听套接字,所以可以把父进程的sock关闭,而且是必须关掉,因为这个sock对它没用了close(_sock);}}~tcpServer(){close(listen_sock);}
};

以上代码中关于父子进程之间的关系,以及进程等待这里就不细谈了,详见

Linux系统编程

再次强调,对于子进程,它可以关闭监听套接字,因为子进程是用来通信的,它只关心sock,对于父进程则必须要关闭sock,只保留监听套接字,否则客户端连接的越多,系统资源将会被耗尽

再次测试,可以发现多个客户端可以同时连接服务器

(2)多线程版本

在Linux系统编程那一部分我们详细说过多线程和多进程的优缺点,创建进程的代价远远高于创建线程,因此多线程版本如下

#include <iostream>
#include <pthread.h>
#include <signal.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;#define  BACKLOG 5class tcpServer
{private:int _port;int listen_sock;
public:tcpServer(int port=8080):_port(port){}void initServer(){listen_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字创建失败"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"绑定失败"<<endl;exit(3);}//开启监听if(listen(listen_sock,BACKLOG) < 0){cout<<"绑定失败"<<endl;exit(4);}}static void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';cout<<"服务端收到客户端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);}if(s==0){cout<<"客户端已经下线"<<endl;close(sock);break;}}}static void* Route(void* args)//线程路线{pthread_detach(pthread_self());int* p=(int*)args;int sock=*p;Service(sock);return NULL;}void startServer(){sockaddr_in endpoint;while(1){//acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失败"<<endl;continue;}string client_info=inet_ntoa(endpoint.sin_addr);client_info+=":";client_info+=to_string(ntohs(endpoint.sin_port));cout<<"一台新的客户端已经连接:"<<client_info<<endl;pthread_t tid;int* p=new int(sock);//在堆上开辟,p是栈私有的,防止出现bugpthread_create(&tid,nullptr,Route,(void*)&_sock);}}~tcpServer(){close(listen_sock);}
};

效果如下

(3)线程池版本

关于线程部分,可以查看

Linux系统编程41:多线程之线程池的概念及实现

服务器大致逻辑为:

  1. 初始化服务器,服务器初始化时创建线程池
  2. 启动服务器,创建任务,将任务放进线程池的任务队列中
  3. 由于任务的加入,唤醒了线程池中的线程,线程拿到任务,调用任务的工作接口,进行工作
  4. 工作完毕调用析构函数,释放套接字

下面的代码简单的实现了客户端发送英文单词,服务端进行翻译的过程

thread_pool.h

#pragma once
#include <iostream>
#include <pthread.h>
#include <signal.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <queue>
#include <map>
using namespace std;struct Task//任务
{private:int _sock;//套接字static map<string,string> dict;//字典
public:Task(){}~Task(){close(_sock);}Task(int sock):_sock(sock){dict.insert(pair<string,string>("apple","苹果"));dict.insert(pair<string,string>("melon","西瓜"));dict.insert(pair<string,string>("orange","橘子"));}void work()//获得任务后在这里进行判断{char buffer[1024];size_t s=recv(_sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';string key=buffer;//拿到用户的键值cout<<"服务端收到客户端的消息:"<<buffer<<endl;send(_sock,dict[key].c_str(),dict[key].size(),0);//给用户返回翻译结果}if(s==0){cout<<"客户端已经下线"<<endl;}}
};map<string,string>Task:: dict;class ThreadPool//线程池
{private:bool _thread_quit_flag;//线程退出标志,如果主线程发送了信号,就将其置为trueint _thread_num;//线程池线程数量queue<Task*> q;//任务队列,存放指针pthread_mutex_t lock;//保护任务队列的锁pthread_cond_t cond;//条件变量,当没有任务时线程池的线程挂起,当有任务时唤醒线程池线程public:void ThreadLock(){//锁pthread_mutex_lock(&lock);}void ThreadUnlock(){//解锁pthread_mutex_unlock(&lock);}bool IsEmpty(){//判断任务队列为空return q.size()==0;}void Threadwait(){//没有任务线程池的线程挂起pthread_cond_wait(&cond,&lock);}void Threadwakeup(){//当主线程添加了任务就唤醒线程池线程pthread_cond_signal(&cond);}void ThreadWakeAll(){//最后发送退出信号时,将所有线程唤醒,然后让其退出pthread_cond_broadcast(&cond);//唤醒所有线程}void ThreadQuit(){//线程退出函数pthread_exit(nullptr);}public:ThreadPool(int thread_num=5):_thread_quit_flag(false),_thread_num(thread_num)//flag默认设置为flase,不退出{pthread_mutex_init(&lock,nullptr);pthread_cond_init(&cond,nullptr);}~ThreadPool(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}static void* Route(void* arg)//所有的线程都要执行这样的流程:先判断是否有任务,有就执行,没有就挂起{pthread_detach(pthread_self());//线程 ThreadPool* this_p=(ThreadPool*)arg;//某个线程的this指针while(1){this_p->ThreadLock();//锁定任务队列while(this_p->_thread_quit_flag==false && this_p->IsEmpty())//如果没有发出退出信号并且队列为空,那么就让线程挂起{this_p->Threadwait();//当线程苏醒时(可能接收到了广播信号或者位于线程队列它被下一个唤醒了),就会从挂起的地方醒来,继续向下执行}if((this_p->_thread_quit_flag==false && !this_p->IsEmpty()) || (this_p->_thread_quit_flag==true && !this_p->IsEmpty()))//醒来的原因不管是否是因为接收到了退出信号,反正只要有任务就得先执行完{Task* t;this_p->Get(&t);this_p->ThreadUnlock();//注意不要在临界资源内做任务,效率很低t->work();//获得任务后进行计算delete t;//任务结束关闭套接字,调用任务析构函数关闭套接字}//如果接受到信号且队列已经Wie空了,那么就退出线程else if(this_p->_thread_quit_flag==true && this_p->IsEmpty())//如果醒来的原因是因为接受到了退出信号,而且任务队列中已经没有任务了,那么就退出线程{this_p->ThreadUnlock();this_p->ThreadQuit();//退出时不要忘记解锁,否则将来只有一个线程能推出,其余线程无法退出}}
}void ThreadPoolInit()//风险操作不要在构造函数中写,该函数用于创建线程池线程,并让线程执行Route流程{pthread_t t;for(int i=0;i < _thread_num;i++){pthread_create(&t,nullptr,Route,this);//这里最后一个参数传入了this指针,相应的Route函数也是静态成员函数//如果不传入this指针,那么当这个参数就会和非静态成员函数抢this指针的位置,因此会造成参数过多的问题//所以在这里传入this,然后在形参中使用this调用自己的成员函数即可}}void ThreadPoolQuit()//线程池退出{_thread_quit_flag=true;//将结束标志置为trueThreadWakeAll();//唤醒所有线程}void Get(Task** t)//线程获取任务{Task* out=q.front();//注意是指针q.pop();*t=out;//这是解引用}void Put(Task& t)//主线程放任务{ThreadLock();q.push(&t);//注意这是取地址ThreadUnlock();Threadwakeup();//当放了一个任务后立马唤醒线程,这里一般情况下不要唤醒所有线程,会产生掠群效应,影响稳定性}
};

tcp_server.h

#include "thread_pool.h"
#define  BACKLOG 5class tcpServer
{private:int _port;int listen_sock;ThreadPool* _th;
public:tcpServer(int port=8080):_port(port),_th(nullptr){}void initServer(){listen_sock=socket(AF_INET,SOCK_STREAM,0);//区别UDP,TCP采用流式套接字if(listen_sock < 0){cout<<"套接字创建失败"<<endl;exit(2);}struct sockaddr_in local;local .sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=htonl(INADDR_ANY);if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0){cout<<"绑定失败"<<endl;exit(3);}//开启监听if(listen(listen_sock,BACKLOG) < 0){cout<<"绑定失败"<<endl;exit(4);}_th=new ThreadPool();_th->ThreadPoolInit();//初始化线程池}static void Service(int sock){char buffer[1024];while(1){size_t s=recv(sock,buffer,sizeof(buffer)-1,0);if(s > 0){buffer[s]='\0';cout<<"服务端收到客户端的消息:"<<buffer<<endl;send(sock,buffer,strlen(buffer),0);}if(s==0){cout<<"客户端已经下线"<<endl;close(sock);break;}}}static void* Route(void* args){pthread_detach(pthread_self());int* p=(int*)args;int sock=*p;Service(sock);return NULL;}void startServer(){sockaddr_in endpoint;while(1){//acceptsocklen_t len=sizeof(endpoint);int _sock=accept(listen_sock,(struct sockaddr*)&endpoint,&len);if(_sock < 0){cout<<"accept失败"<<endl;continue;}string client_info=inet_ntoa(endpoint.sin_addr);client_info+=":";client_info+=to_string(ntohs(endpoint.sin_port));cout<<"一台新的客户端已经连接:"<<client_info<<endl;//构建任务,放进线程池Task* t=new Task(_sock);_th->Put(*t);}}~tcpServer(){close(listen_sock);}};

tcp_client.h

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;class tcpClient
{private:string _ip;int _port;int _sock;
public:tcpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port){}void initClient(){_sock=socket(AF_INET,SOCK_STREAM,0);if(_sock < 0){cout<<"套接字创建失败"<<endl;exit(2);}struct sockaddr_in sev;sev.sin_family=AF_INET;sev.sin_port=htons(_port);sev.sin_addr.s_addr=inet_addr(_ip.c_str());if(connect(_sock,(struct sockaddr*)&sev,sizeof(sev))!=0){cout<<"connect失败"<<endl;exit(3);}}void startClient(){char mssage[64];while(1){   cout<<"请输入信息**:";fflush(stdout);size_t s=read(0,mssage,sizeof(mssage)-1);//从标准输入读入if(s > 0){mssage[s-1]='\0';//剔除换行符send(_sock,mssage,strlen(mssage),0);ssize_t ret=recv(_sock,mssage,sizeof(mssage)-1,0);if(ret > 0){cout<<"客户端得到服务端消息"<<mssage<<endl;}else if(ret ==0){break;}}}}~tcpClient(){close(_sock);}
};

效果如下

2-4:套接字(Socket)编程之TCP通信相关推荐

  1. 基于Linux的Socket编程之TCP全双工Server-Client聊天程序

    转载:http://blog.csdn.net/apollon_krj/article/details/53437764#0-tsina-1-58570-397232819ff9a47a7b7e80a ...

  2. Linux下Socket编程之TCP应用

    现在,我们用前面所构建的socket类,重新设计<Linux下Socket编程之TCP Server端>中echo的服务器,然后设计客户端程序. echo服务器的工作原理很简单: 1.接收 ...

  3. Socket编程之TCP实例(附C/C++代码详解)

    说明: 主要分步骤给出Windows平台下socket编程的一个TCP实例:使用WINDOWS下网络编程规范Winsock完成网络通信: 对程序各部分细节进行描述. 套接字有三种传输类型SOCK_ST ...

  4. Windows Socket编程之TCP实现大文件的传输

    前言: UDP版本可参考被人博客: http://blog.csdn.net/luchengtao11/article/details/71016222 本文所述代码工程托管在Github: http ...

  5. linux socket编程之TCP与UDP

    转:http://blog.csdn.net/gaoxin1076/article/details/7262482 TCP/IP协议叫做传输控制/网际协议,又叫网络通信协议 TCP/IP虽然叫传输控制 ...

  6. 基于Linux的SOCKET编程之TCP半双工Client-Server聊天程序

    转自:http://blog.csdn.net/apollon_krj/article/details/53398448#0-tsina-1-64987-397232819ff9a47a7b7e80a ...

  7. Linux C socket 编程之TCP

    本文主要是,简单实现tcp连接的两个程序.本文编写,假设读者有socket 编程思想.熟悉C编程. 服务端: #include <stdio.h> #include <stdlib. ...

  8. Linux下Socket编程之TCP Server端

    一.建模 绝大部分关于socket编程的教程总是从socket的概念开始讲起的.要知道,socket的初衷是个庞大的体系,TCP/IP只是这个庞大体系下一个很小的子集,而我们真正能用上的更是这个子集中 ...

  9. windows Socket编程之TCP服务端与客户端

    在前面的文章中有一篇讲到了命名管道通信,它是创建一根管道来进行进程之间或网络之间通信的.但是它有些缺陷,比如说效率较低等.而从这篇文章开始将介绍socket编程.socket是通过TCP,UDP,IP ...

最新文章

  1. Impala与Hive的比较
  2. [题解]BZOJ1004 序列函数
  3. react 技术栈项目轻量化方案调研
  4. ExtJs Ext.TaskMgr定时刷新数据源
  5. tensorflow中使用tf.ConfigProto()配置Session运行参数GPU设备指定
  6. androidx86 9.0下载_13.3寸大屏安卓9.0强悍性能刷新你认知!BOOX MAX3电纸书上手测评...
  7. System verilog随机系统函数$random使用方法
  8. Apache2.4.x与Apache2.2.x的一些区别
  9. mfc 如何判断excel软件是否打开_如何从无到有地搭建一套完整的测试系统(上)...
  10. win10系统的安装
  11. 服务器是计算机硬件嘛,什么是服务器,服务器是软件还是硬件?
  12. 两台电脑间使用网线连接实现共享
  13. 已会背诵英文文章 How To Boost Your Confidence
  14. office2007 ppt制作与应用母板
  15. flash 与3D笔记:图片墙(1)
  16. Xshell7工具下载安装以及简单使用
  17. sql查询当天 当月 当年
  18. 如何设计出性能更优的MySQL数据库schema?
  19. Zynq7000系列之芯片引脚功能合集以及引脚分配
  20. Sam Altman的成功学|升维指南

热门文章

  1. 第四节:HTML5给表单带来的新标签、新属性、新类型
  2. 基于Flume的美团日志收集系统-----架构和设计
  3. 一行代码添加P值的可视化技巧分享~~
  4. 他为华裔写了几封信......
  5. 英雄?好汉?可否有人站出来回应下?
  6. FCPX Full Access - Titles Bundle for Mac FCPX标题字幕插件捆绑包
  7. 1.3编程基础之算术表达式与顺序执行 01 A+B问题
  8. ERP(进纯销)系统如何开发,各类型系统都应该怎么做?
  9. phpStrom编辑器常用功能教程
  10. php 获取子孙,jquery如何查找后代元素?jquery获取后代元素方法