目录

一、客户端实现

二、单进程服务器

2.1 单进程实现

2.2 单进程非阻塞实现

2.3 TCP服务器(select版)

2.4 epoll版服务器实现

三、多进程服务器和多线程服务器

四、协程

4.1 协程的生成器实现

4.2 协程的greenlet实现

4.3 协程的gevent实现

4.3.1 gevent的使用

4.3.2 gevent的切换执行

4.3.3 gevent的服务器实现

一、客户端实现

客户端比较简单,并且适用于与不同服务器通信,代码如下:

#coding=utf-8
from socket import *
import random
import time
serverIp = raw_input("请输⼊服务器的ip:")
connNum = raw_input("请输⼊要链接服务器的次数(例如1000):")
g_socketList = []
for i in range(int(connNum)):
s = socket(AF_INET, SOCK_STREAM)
s.connect((serverIp, 7788))
g_socketList.append(s)
print(i)
while True:
for s in g_socketList:
s.send(str(random.randint(0,100)))
# ⽤来测试⽤
#time.sleep(1)

二、单进程服务器

2.1 单进程实现

Linux服务器开发学习视频资料,包括Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等等,需要知识技术学习视频文档资料的朋友可以加我群720209036获取

单进程完成一个tcp服务器,同时只能为一个客户端服务。

from socket import *
serSocket = socket(AF_INET, SOCK_STREAM)
# 重复使⽤绑定的信息,若服务器先close,则不用等待2MSL,可以直接绑定下一个客户端
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
while True:
print('-----主进程, , 等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('-----主进程, , 接下来负责数据处理[%s]-----'%str(destAddr))
try:
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
finally:
newSocket.close()
serSocket.close()

2.2 单进程非阻塞实现

上面单进程实现同时只能为一个服务端服务,如果第二个while中不阻塞,则可以实现多用户同时服务。代码如下:

#coding=utf-8
from socket import *
import time
# ⽤来存储所有的新链接的socket
g_socketList = []
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
#可以适当修改listen中的值来看看不同的现象
serSocket.listen(1000)
#将套接字设置为⾮堵塞
#设置为⾮堵塞后, 如果accept时, 恰巧没有客户端connect, 那么accept会
#产⽣⼀个异常, 所以需要try来进⾏处理
serSocket.setblocking(False)
while True:
#⽤来测试
#time.sleep(0.5)
try:
newClientInfo = serSocket.accept()
except Exception as result:
pass
else:
print("⼀个新的客户端到来:%s"%str(newClientInfo))
newClientInfo[0].setblocking(False)
g_socketList.append(newClientInfo)
# ⽤来存储需要删除的客户端信息
needDelClientInfoList = []
# 为列表中每个客户端服务
for clientSocket,clientAddr in g_socketList:
try:
recvData = clientSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(clientAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(clientAddr))
clientSocket.close()
g_needDelClientInfoList.append((clientSocket,clientAddr))
except Exception as result:
pass
for needDelClientInfo in needDelClientInfoList:
g_socketList.remove(needDelClientInfo)
if __name__ == '__main__':
main()

2.3 TCP服务器(select版)

tcp/ip学习文档代码资料加我群720209036获取

在非阻塞版本中使用for循环为列表中的每个客户端服务,而select版是通过调用select函数直接返回列表中接收到数据的socket,不必循环遍历。

优点:几乎所有平台都支持,有良好的跨平台性。

缺点:select的⼀个缺点在于单个进程能够监视的⽂件描述符的数量存在最⼤限制,在Linux上⼀般为1024, 可以通过修改宏定义甚⾄重新编译内核的⽅式提升这⼀限制, 但是这样也会造成效率的降低。

⼀般来说这个数⽬ 和系统内存关系很⼤, 具体数⽬ 可以cat /proc/sys/fs/filemax查看。 32位机默认是1024个。 64位机默认是2048.个。对socket进⾏扫描时是依次扫描的, 即采⽤轮询的⽅法, 效率较低。

当套接字⽐较多的时候, 每次select()都要通过遍历FD_SETSIZE个Socket来完成调度, 不管哪个Socket是活跃的, 都遍历⼀遍。 这会浪费很多CPU时间。

select函数解释如图:

select版tcp服务器代码如下:

import select
import socket
import sys
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 7788))
server.listen(5)
inputs = [server, sys.stdin]
running = True
while True:
# 调用 select 函数,阻塞等待
readable, writeable, exceptional = select.select(inputs, [], [])
# 数据抵达,循环
for sock in readable:
# 监听到有新的连接
if sock == server:
conn, addr = server.accept()
# select 监听的socket
inputs.append(conn)
# 监听到键盘有输入
elif sock == sys.stdin:
cmd = sys.stdin.readline()
running = False
break
# 有数据到达
else:
# 读取客户端连接发送的数据
data = sock.recv(1024)
if data:
sock.send(data)
else:
# 移除select监听的socket
inputs.remove(sock)
sock.close()
# 如果检测到用户输入敲击键盘,那么就退出
if not running:
break
server.close()

2.4 epoll版服务器实现

为了解决select版并发连接数目的限制,出现了poll版,与select版几乎相同,唯一不同的是数量不受限制,仍是用的轮询方式。后来为了解决poll版轮询监测方式低下的问题出现了epoll版,epoll版相当于“有问题举手”,而不是“挨个问是否有问题”。

epoll版的优点:

1. 没有最⼤并发连接的限制, 能打开的FD(指的是⽂件描述符), 通俗的理解就是套接字对应的数字编号)的上限远⼤于1024

2. 效率提升, 不是轮询的⽅式, 不会随着FD数⽬的增加效率下降。 只有活跃可⽤的FD才会调⽤callback函数; 即epoll最⼤的优点就在于它只管你“活跃”的连接, ⽽跟连接总数⽆关, 因此在实际的⽹络环境中, epoll的效率就会远远⾼于select和poll。

epoll版tcp服务器代码如下:

代码解释:

epoll的三种事件:

EPOLLIN (可读)

EPOLLOUT (可写)

EPOLLET (ET模式)

epoll对⽂件描述符的操作有两种模式: LT(level trigger 水平触发) 和ET(edge trigger 边沿触发) 。 LT模式是默认模式, LT模式与ET模式的区别如下:

LT模式: 当epoll检测到描述符事件发⽣并将此事件通知应⽤程序, 应⽤程序可以不⽴即处理该事件

ET模式: 当epoll检测到描述符事件发⽣并将此事件通知应⽤程序, 应⽤程序必须⽴即处理该事件,否则会丢失

import socket
import select
# 创建套接字
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 设置可以重复使⽤绑定的信息
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定本机信息
s.bind(("",7788))
# 变为被动
s.listen(10)
# 创建⼀个epoll对象
epoll=select.epoll()
# 测试, ⽤来打印套接字对应的⽂件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET
# 注册事件到epoll中
# epoll.register(fd, [eventmask])
# 注意, 如果fd已经注册过, 则会发⽣异常
# 将创建的socket添加到epoll的事件监听中
# [eventmask]为监听的事件列表,有列表中的事件时才会放入epoll列表,
# 事件有三种,EPOLLIN接收数据事件,EPOLLOUT发送数据,EPOLLET模式(水平触发或边沿触发)
epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET)
# connections用于存储socket,addresses用于存储端口,
# 它们都为字典,key为socket的文件描述符,value为socket或端口!
connections = {}
addresses = {}
# 循环等待客户端的到来或者对⽅发送数据
while True:
# epoll 进⾏ fd 扫描的地⽅ -- 未指定超时时间则为阻塞等待
# 等价于select版本中的 readable,xxx,yyy = select([],[],[])
# 不为轮询,使用的是事件通知机制,为本代码的核心
epoll_list=epoll.poll()
# 对事件进⾏判断
for fd,events in epoll_list:
# print fd
# print events
# 如果是socket创建的套接字被激活
if fd == s.fileno():
conn,addr=s.accept()
print('有新的客户端到来%s'%str(addr))
# 将 conn 和 addr 信息分别保存起来
# 注意connections和address为字典,以key、value存储
connections[conn.fileno()] = conn
addresses[conn.fileno()] = addr
# 向 epoll 中注册 连接 socket 的 可读 事件
epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
elif events == select.EPOLLIN:
# 从激活 fd 上接收
recvData = connections[fd].recv(1024)
if len(recvData)>0:
print('recv:%s'%recvData)
else:
# 从 epoll 中移除该 连接 fd
epoll.unregister(fd)
# server 则主动关闭该 连接 fd
connections[fd].close()
print("%s---offline---"%str(addresses[fd]))

三、多进程服务器和多线程服务器

代码说明:

1、多进程实现和多线程实现几乎相同,不同点:1、创建的时候;2、while中多进程实现中,由于子进程复制了一份,所以可以关闭,多线程中,子线程之间共享资源,所以在while中不能关闭。

2、代码中使用try...finally,目的是可以使用Ctrl+C强制结束进程或线程。

多进程服务器代码如下:

#coding=utf-8
from socket import *
from multiprocessing import *
from time import sleep
# 处理客户端的请求并为其服务
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主进程,,等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('-----主进程,,接下来创建⼀个新的进程负责数据处理------')
client = Process(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因为已经向⼦进程中copy了⼀份(引⽤),并且⽗进程中这个套接字所以关闭
newSocket.close()
finally:
#当为所有的客户端服务完之后再进⾏关闭,表示不再接收新的客户端的链接
serSocket.close()
if __name__ == '__main__':
main()

多线程服务器代码如下:

#coding=utf-8
from socket import *
from threading import Thread
from time import sleep
# 处理客户端的请求并执⾏事情
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主进程, , 等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('-----主进程, , 接下来创建⼀个新的进程负责数据处理[%s]-----')
client = Thread(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因为线程中共享这个套接字, 如果关闭了会导致这个套接字不可⽤,
#但是此时在线程中这个套接字可能还在收数据, 因此不能关闭
#newSocket.close()
finally:
serSocket.close()
if __name__ == '__main__':
main()

四、协程

协程文档代码学习资料加我群720209036获取

进程里面有线程,线程里面有协程。协程不牵扯到切换,并且能完成多任务。

注意:计算密集型时用多进程;IO密集型使用多线程、多协程。

通俗的理解: 在⼀个线程中的某个函数, 可以在任何地⽅保存当前函数的⼀些临时变量等信息, 然后切换到另外⼀个函数中执⾏, 注意不是通过调⽤函数的⽅式做到的, 并且切换的次数以及什么时候再切换到原来的函数都由开发者⾃⼰确定。

协程和线程的区别:线程切换从系统层⾯远不⽌保存和恢复 CPU上下⽂这么简单。 操作系统为了程序运营的⾼效性每个线程都有⾃⼰缓存Cache等等数据, 操作系统还会帮你做这些数据的恢复操作。所以线程的切换⾮常耗性能。 但是协程的切换只是单纯的操作CPU的上下⽂, 所以⼀秒钟切换个上百万次系统都抗的住。

协程调度:操作系统不感知协程,所以操作系统不会对协程调度。 ⽬前的协程框架⼀般都是设计成 1:N 模式。 所谓 1:N 就是⼀个线程作为⼀个容器⾥⾯放置多个协程。 那么谁来适时的切换这些协程? 答案是有协程自己主动让出CPU, 也就是每个协程池⾥⾯有⼀个调度器, 这个调度器是被动调度的。 意思就是他不会主动调度。 ⽽且当⼀个协程发现自己执行不下去了(比如异步等待⽹络的数据回来, 但是当前还没有数据到), 这个时候就可以由这个协程通知调度器, 这个时候执⾏到调度器的代码, 调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下⽂把CPU的运⾏权交给这个协程, 直到这个协程出现执行不下去需要等等的情况, 或

者它调⽤主动让出CPU的API之类, 触发下⼀次调度。

协程调度存在问题:假设一个线程中有⼀个协程是CPU密集型的他没有IO操作, 也就是自己不会主动触发调度器调度的过程, 那么就会出现其他协程得不到执⾏的情况, 所以这种情况下需要程序员⾃ ⼰避免。 这是⼀个问题, 假设业务开发的⼈员并不懂这个原理的话就可能会出现问题。

协程的优点:在IO密集型的程序中由于IO操作远远慢于CPU的操作, 所以往往需要CPU去等IO操作。 同步IO下系统需要切换线程, 让操作系统可以在IO过程中执⾏其他的东⻄。 这样虽然代码是符合⼈类的思维习惯但是由于⼤量的线程切换带来了⼤量的性能的浪费, 尤其是IO密集型的程序。

所以⼈们发明了异步IO。 就是当数据到达的时候触发我的回调。 来减少线程切换带来性能损失。 但是这样的坏处也是很⼤的, 主要的坏处就是操作被“分片” 了, 代码写的不是 “一气呵成” 这种。 而是每次来段数据就要判断 数据够不够处理, 够处理就处理, 不够处理就再等等。 这样代码的可读性很低, 其实也不符合⼈类的习惯。

但是协程可以很好解决这个问题。 比如 把⼀个IO操作 写成⼀个协程。 当触发IO操作的时候就自动让出CPU给其他协程。 要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在⾼IO密集型的程序下很好。 但是高CPU密集型的程序下没啥好处。

4.1 协程的生成器实现

协程使用生成器来实现的,代码如下(只切换了函数调用,所以效率比较高):

import time
def A():
while True:
print("----A---")
yield
time.sleep(0.5)
def B(c):
while True:
print("----B---")
c.next()
time.sleep(0.5)
if __name__=='__main__':
a = A()
B(a)

结果如下:

--B--
--A--
--B--
--A--
--B--
--A--
--B--
--A--
...省略...

4.2 协程的greenlet实现

与生成器实现类似。

注意:进程、线程的调用是操作系统决定的,执行顺序不可预测,而协程是程序员决定的执行顺序可预测,这由以下代码可知(当执行到xx.switch()时会切换)。

使用下面命令安装greenlet:

sudo pip install greenlet #python2的安装方式
sudo pip3 install greenlet #python3的安装方式

协程的greenlet实现代码如下:

#coding=utf-8
from greenlet import greenlet
import time
def test1():
while True:
print "---A--"
gr2.switch() # 切换到gr2(即test2)中执行,test2执行切换时会从当前接着执行
time.sleep(0.5)
def test2():
while True:
print "---B--"
gr1.switch() # 切换到gr1(即test1)中执行,test1执行切换时会从当前接着执行
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
#切换到gr1(即test1函数)中执行
gr1.switch()

结果如下:

--A--
--B--
--A--
--B--
--A--
--B--
--A--
...省略...

4.3 协程的gevent实现

gevent是对greenlet的再次封装,不用程序员自己编程切换,当遇到需要切换的地方会自动切换。

4.3.1 gevent的使用

#coding=utf-8
#请使⽤python 2 来执⾏此程序
import gevent
def f(n):
for i in range(n):
print gevent.getcurrent(), i
g1 = gevent.spawn(f, 5) # 绑定f函数,执行5次
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
# 清除协程
g1.join()
g2.join()
g3.join()

执行结果:

一瞬间执行完毕,g1、g2、g3依次顺序执行,并非交替执行,不是我们想要的结果。

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4

4.3.2 gevent的切换执行

上面顺序执行的原因是在f函数中没有调用延时,所以不会切换。gevent当遇到耗时操作时才会切换,所以增加一个延时函数使它能够切换,代码如下:

import gevent
def f(n):
for i in range(n):
print gevent.getcurrent(), i
#⽤来模拟⼀个耗时操作, 注意不是time模块中的sleep
gevent.sleep(1)
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

结果如下:

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

4.3.3 gevent的服务器实现

注意:要使用gevent实现服务器,不能使用默认的socket,而是使用gevent自己的socket,gevent将常用的耗时操作都重写了一遍,用于检测是否为耗时操作。

import sys
import time
import gevent
from gevent import socket,monkey
# 此语句会将本代码改写,位于编译器级的,具体不清楚!(python为动态语言在执行中可以修改)
# 必须使用!!!
monkey.patch_all()
def handle_request(conn):
while True:
#--------------#1处#-----------------
data = conn.recv(1024) # 这是gevent中的recv,为耗时操作,会切换到2处!
if not data:
conn.close()
break
print("recv:", data)
conn.send(data)
def server(port):
s = socket.socket()
s.bind(('', port))
s.listen(5)
while True:
#--------------#2处#-----------------
cli, addr = s.accept() # 这是gevent中的accept,为耗时操作,会进行切换!
# 注意:第一次到这里时只有一个协程,不需要切换,在此等待!
# 不为第一次时切换到1处!
gevent.spawn(handle_request, cli)
if __name__ == '__main__':
server(7788)

socket编程listen函数限制连接数的解决方案_网络编程——服务器篇相关推荐

  1. UNIX网络编程-listen函数及其包裹函数介绍

    UNIX网络编程-listen函数及其包裹函数介绍 函数简介 #include<sys/socket.h>int listen(int sockfd,int backlog);返回:若成功 ...

  2. 网络编程socket之listen函数

    listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程.在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接 ...

  3. LwIP tcp/ip socket编程listen函数分析

    函数原型为: [cpp] view plain copy int listen(int  sockfd, int  backlog); <span style="font-family ...

  4. js监听select值变化_网络编程——C++实现socket通信(TCP)高并发之select模式

    相关函数: 服务端: socket() bind() listen() FD_ZERO()等辅助函数 select() 高并发select模式 accept() read() 或 recv()等 wr ...

  5. 聊聊Socket、TCP/IP、HTTP、FTP及网络编程

    1 这些都是什么 既然是网络传输,涉及几个系统之间的交互,那么首先要考虑的是如何准确的定位到网络上的一台或几台主机,另一个是如何进行可靠高效的数据传输.这里就要使用到TCP/IP协议. 1.1 TCP ...

  6. python的基础网络编程是下列_Python基础(六)网络编程

    Socket socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. sock ...

  7. weblogic多次连接后tcp服务堵塞_网络编程——服务器篇

    目录 一.客户端实现 二.单进程服务器 2.1 单进程实现 2.2 单进程非阻塞实现 2.3 TCP服务器(select版) 2.4 epoll版服务器实现 三.多进程服务器和多线程服务器 四.协程 ...

  8. 第六周 Java语法总结_设计原则_工厂模式_单例模式_代理模式(静态代理_动态代理)_递归_IO流_网络编程(UDP_TCP)_反射_数据库

    文章目录 20.设计原则 1.工厂模式 2.单例模式 1)饿汉式 2)懒汉式 3.Runtime类 4.代理模式 1)静态代理 2)动态代理 动态代理模板 21.递归 22.IO流 1.File 2. ...

  9. 【TCP/IP网络编程】(01):什么是网络编程

    什么是网络编程 网络编程简单来说就是编程使得计算机之间交互数据. 硬件部分已经接入了互联网中,我们需要考虑的是软件部分. 软件部分有OS提供的套接字. 套接字 套接字用于网络数据传输,这个名字看上去很 ...

最新文章

  1. SQL 统计表 b 对应表a个数,并赋值给表a的一个字段
  2. linux dd命令制作软盘,制作Linux启动软盘的四种方法
  3. 关于echars中雷达图的一些配置
  4. js时间搓化为今天明天_护肤品搓泥怎么办啊啊啊啊啊......
  5. docker修改容器名字
  6. 数据结构与算法学习笔记之 从0编号的数组
  7. php include不可用,无法设置PHP include_path
  8. 基于python的音乐推荐系统
  9. com.android.htcdialer+意外停止+htc,HTC刷机为什么会变砖 HTC刷机变砖修复方法介绍【教程】...
  10. TC软件概要设计文档(手机群控)
  11. Java学习笔记——十大经典排序算法总结
  12. 解读《一首有趣的回环诗~秦观》
  13. RepLKNet:不是大卷积不好,而是卷积不够大,31x31卷积了解一下 | CVPR 2022
  14. java nio 学习笔记2
  15. Adb使用教程,看了教程后你也是玩机达人啦
  16. php 网址尾部带斜杠和不带区别,网址中带斜杠和不带斜杠的区别
  17. html播放音乐怎么隐藏,酷狗音乐隐藏的小秘密 咋让听歌更方便
  18. Prometheus监控神技--自动发现配置
  19. git revert回退时提示One or more files are in a conflicted state
  20. 用DirectX绘制使用纹理的立方体

热门文章

  1. grantrevoke
  2. 极光商智®服务器2007今日正式发布
  3. CentOS安装网卡设置
  4. Fedora开启ssh服务
  5. 动态规划(最长递增子序列)---最长递增子序列
  6. Linux文件权限(3)
  7. Laya 位图字体制作(失败...)
  8. fresco的使用教程
  9. Centos下MongoDB的安装与配置
  10. 关于新版SDK报错You need to use a Theme.AppCompat theme的两种解决办法 - 转