使用多线程实现多客户端连接服务端

流程图



服务端代码改造:

package socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket; /**
* 聊天室服务端
*/
public class Server {
/**
* 运行在服务端的ServerSocket主要完成两个工作:
* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接
* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket
* 就可以和该客户端交互了
*
* 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个
* 电话使得服务端与你沟通。
*/
private ServerSocket serverSocket; /**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
/*
实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他
应用程序占用的端口相同,否则会抛出异常:
java.net.BindException:address already in use 端口是一个数字,取值范围:0-65535之间。
6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
} /**
* 服务端开始工作的方法
*/
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
/*
ServerSocket提供了接受客户端链接的方法:
Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例
通过这个Socket就可以与客户端进行交互了。 可以理解为此操作是接电话,电话没响时就一直等。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程与该客户端交互
ClientHandler clientHandler = new ClientHandler(socket);
Thread t = new Thread(clientHandler);
t.start(); }
} catch (IOException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
Server server = new Server();
server.start();
} /**
* 定义线程任务
* 目的是让一个线程完成与特定客户端的交互工作
*/
private class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket = socket;
}
public void run(){
try{
/*
Socket提供的方法:
InputStream getInputStream()
获取的字节输入流读取的是对方计算机发送过来的字节
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr); String message = null;
while ((message = br.readLine()) != null) {
System.out.println("客户端说:" + message);
}
}catch(IOException e){
e.printStackTrace();
}
}
} }

线程API

获取线程相关信息的方法

package thread;

/**
* 获取线程相关信息的一组方法
*/
public class ThreadInfoDemo {
public static void main(String[] args) {
Thread main = Thread.currentThread();//获取主线程 String name = main.getName();//获取线程的名字
System.out.println("名字:"+name); long id = main.getId();//获取该线程的唯一标识
System.out.println("id:"+id); int priority = main.getPriority();//获取该线程的优先级
System.out.println("优先级:"+priority); boolean isAlive = main.isAlive();//该线程是否活着
System.out.println("是否活着:"+isAlive); boolean isDaemon = main.isDaemon();//是否为守护线程
System.out.println("是否为守护线程:"+isDaemon); boolean isInterrupted = main.isInterrupted();//是否被中断了
System.out.println("是否被中断了:"+isInterrupted); }
}

线程优先级

线程start后会纳入到线程调度器中统一管理,线程只能被动的被分配时间片并发运行,而无法主动索取时间片.线程调度器尽可能均匀的将时间片分配给每个线程.

线程有10个优先级,使用整数1-10表示

  • 1为最小优先级,10为最高优先级.5为默认值
  • 调整线程的优先级可以最大程度的干涉获取时间片的几率.优先级越高的线程获取时间片的次数越多,反之则越少.
package thread;

public class PriorityDemo {
public static void main(String[] args) {
Thread max = new Thread(){
public void run(){
for(int i=0;i<10000;i++){
System.out.println("max");
}
}
};
Thread min = new Thread(){
public void run(){
for(int i=0;i<10000;i++){
System.out.println("min");
}
}
};
Thread norm = new Thread(){
public void run(){
for(int i=0;i<10000;i++){
System.out.println("nor");
}
}
};
min.setPriority(Thread.MIN_PRIORITY);
max.setPriority(Thread.MAX_PRIORITY);
min.start();
norm.start();
max.start();
}
}

sleep阻塞

线程提供了一个静态方法:

  • static void sleep(long ms)
  • 使运行该方法的线程进入阻塞状态指定的毫秒,超时后线程会自动回到RUNNABLE状态等待再次获取时间片并发运行.
package thread;

public class SleepDemo {
public static void main(String[] args) {
System.out.println("程序开始了!");
try {
Thread.sleep(5000);//主线程阻塞5秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束了!");
}
}

线程API

sleep阻塞(续)

sleep方法处理异常:InterruptedException.

当一个线程调用sleep方法处于睡眠阻塞的过程中,该线程的interrupt()方法被调用时,sleep方法会抛出该异常从而打断睡眠阻塞.

package thread;

/**
* sleep方法要求必须处理中断异常:InterruptedException
* 当一个线程调用sleep方法处于睡眠阻塞的过程中,它的interrupt()方法被调用时
* 会中断该阻塞,此时sleep方法会抛出该异常。
*/
public class SleepDemo2 {
public static void main(String[] args) {
Thread lin = new Thread(){
public void run(){
System.out.println("林:刚美完容,睡一会吧~");
try {
Thread.sleep(9999999);
} catch (InterruptedException e) {
System.out.println("林:干嘛呢!干嘛呢!干嘛呢!都破了像了!");
}
System.out.println("林:醒了");
}
}; Thread huang = new Thread(){
public void run(){
System.out.println("黄:大锤80!小锤40!开始砸墙!");
for(int i=0;i<5;i++){
System.out.println("黄:80!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("咣当!");
System.out.println("黄:大哥,搞定!");
lin.interrupt();//中断lin的睡眠阻塞
}
};
lin.start();
huang.start();
}
}

守护线程

守护线程也称为:后台线程

  • 守护线程是通过普通线程调用setDaemon(boolean on)方法设置而来的,因此创建上与普通线程无异.
  • 守护线程的结束时机上有一点与普通线程不同,即:进程的结束.
  • 进程结束:当一个进程中的所有普通线程都结束时,进程就会结束,此时会杀掉所有正在运行的守护线程.
package thread;

/**
* 守护线程
* 守护线程是通过普通线程调用setDaemon(true)设置而转变的。因此守护线程创建上
* 与普通线程无异。
* 但是结束时机上有一点不同:进程结束。
* 当一个java进程中的所有普通线程都结束时,该进程就会结束,此时会强制杀死所有正在
* 运行的守护线程。
*/
public class DaemonThreadDemo {
public static void main(String[] args) {
Thread rose = new Thread(){
public void run(){
for(int i=0;i<5;i++){
System.out.println("rose:let me go!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("rose:啊啊啊啊啊啊AAAAAAAaaaaa....");
System.out.println("噗通");
}
}; Thread jack = new Thread(){
public void run(){
while(true){
System.out.println("jack:you jump!i jump!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
};
rose.start();
jack.setDaemon(true);//设置守护线程必须在线程启动前进行
jack.start(); }
}

通常当我们不关心某个线程的任务什么时候停下来,它可以一直运行,但是程序主要的工作都结束时它应当跟着结束时,这样的任务就适合放在守护线程上执行.比如GC就是在守护线程上运行的.

多线程并发安全问题

当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱严重时可能导致系统瘫痪.
临界资源:操作该资源的全过程同时只能被单个线程完成.
package thread;

/**
* 多线程并发安全问题
* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现
* 混乱,严重时可能导致系统瘫痪。
* 临界资源:同时只能被单一线程访问操作过程的资源。
*/
public class SyncDemo {
public static void main(String[] args) {
Table table = new Table();
Thread t1 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
Thread t2 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
/*
static void yield()
线程提供的这个静态方法作用是让执行该方法的线程
主动放弃本次时间片。
这里使用它的目的是模拟执行到这里CPU没有时间了,发生
线程切换,来看并发安全问题的产生。
*/
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
} class Table{
private int beans = 20;//桌子上有20个豆子 public int getBean(){
if(beans==0){
throw new RuntimeException("没有豆子了!");
}
Thread.yield();
return beans--;
}
}

synchronized关键字

synchronized有两种使用方式

  • 在方法上修饰,此时该方法变为一个同步方法
  • 同步块,可以更准确的锁定需要排队的代码片段

同步方法

当一个方法使用synchronized修饰后,这个方法称为"同步方法",即:多个线程不能同时 在方法内部执行.只能有先后顺序的一个一个进行. 将并发操作同一临界资源的过程改为同步执行就可以有效的解决并发安全问题.

package thread;

/**
* 多线程并发安全问题
* 当多个线程并发操作同一临界资源,由于线程切换的时机不确定,导致操作顺序出现
* 混乱,严重时可能导致系统瘫痪。
* 临界资源:同时只能被单一线程访问操作过程的资源。
*/
public class SyncDemo {
public static void main(String[] args) {
Table table = new Table();
Thread t1 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
Thread t2 = new Thread(){
public void run(){
while(true){
int bean = table.getBean();
/*
static void yield()
线程提供的这个静态方法作用是让执行该方法的线程
主动放弃本次时间片。
这里使用它的目的是模拟执行到这里CPU没有时间了,发生
线程切换,来看并发安全问题的产生。
*/
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
} class Table{
private int beans = 20;//桌子上有20个豆子 /**
* 当一个方法使用synchronized修饰后,这个方法称为同步方法,多个线程不能
* 同时执行该方法。
* 将多个线程并发操作临界资源的过程改为同步操作就可以有效的解决多线程并发
* 安全问题。
* 相当于让多个线程从原来的抢着操作改为排队操作。
*/
public synchronized int getBean(){
if(beans==0){
throw new RuntimeException("没有豆子了!");
}
Thread.yield();
return beans--;
}
}

同步块

有效的缩小同步范围可以在保证并发安全的前提下尽可能的提高并发效率.同步块可以更准确的控制需要多个线程排队执行的代码片段.

语法:

synchronized(同步监视器对象){
需要多线程同步执行的代码片段
}

同步监视器对象即上锁的对象,要想保证同步块中的代码被多个线程同步运行,则要求多个线程看到的同步监视器对象是同一个.

package thread;

/**
* 有效的缩小同步范围可以在保证并发安全的前提下尽可能提高并发效率。
*
* 同步块
* 语法:
* synchronized(同步监视器对象){
* 需要多个线程同步执行的代码片段
* }
* 同步块可以更准确的锁定需要多个线程同步执行的代码片段来有效缩小排队范围。
*/
public class SyncDemo2 {
public static void main(String[] args) {
Shop shop = new Shop();
Thread t1 = new Thread(){
public void run(){
shop.buy();
}
};
Thread t2 = new Thread(){
public void run(){
shop.buy();
}
};
t1.start();
t2.start();
}
} class Shop{
public void buy(){
/*
在方法上使用synchronized,那么同步监视器对象就是this。
*/
// public synchronized void buy(){
Thread t = Thread.currentThread();//获取运行该方法的线程
try {
System.out.println(t.getName()+":正在挑衣服...");
Thread.sleep(5000);
/*
使用同步块需要指定同步监视器对象,即:上锁的对象
这个对象可以是java中任何引用类型的实例,只要保证多个需要排队
执行该同步块中代码的线程看到的该对象是"同一个"即可
*/
synchronized (this) {
// synchronized (new Object()) {//没有效果!
System.out.println(t.getName() + ":正在试衣服...");
Thread.sleep(5000);
} System.out.println(t.getName()+":结账离开");
} catch (InterruptedException e) {
e.printStackTrace();
} }
}

在静态方法上使用synchronized

当在静态方法上使用synchronized后,该方法是一个同步方法.由于静态方法所属类,所以一定具有同步效果.

静态方法使用的同步监视器对象为当前类的类对象(Class的实例).

注:类对象会在后期反射知识点介绍.

package thread;

/**
* 静态方法上如果使用synchronized,则该方法一定具有同步效果。
*/
public class SyncDemo3 {
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
Boo.dosome();
}
};
Thread t2 = new Thread(){
public void run(){
Boo.dosome();
}
};
t1.start();
t2.start();
}
}
class Boo{
/**
* synchronized在静态方法上使用是,指定的同步监视器对象为当前类的类对象。
* 即:Class实例。
* 在JVM中,每个被加载的类都有且只有一个Class的实例与之对应,后面讲反射
* 知识点的时候会介绍类对象。
*/
public synchronized static void dosome(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName() + ":正在执行dosome方法...");
Thread.sleep(5000);
System.out.println(t.getName() + ":执行dosome方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

静态方法中使用同步块时,指定的锁对象通常也是当前类的类对象

class Boo{
public static void dosome(){
/*
静态方法中使用同步块时,指定同步监视器对象通常还是用当前类的类对象
获取方式为:类名.class
*/
synchronized (Boo.class) {
Thread t = Thread.currentThread();
try {
System.out.println(t.getName() + ":正在执行dosome方法...");
Thread.sleep(5000);
System.out.println(t.getName() + ":执行dosome方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

互斥锁

当多个线程执行不同的代码片段,但是这些代码片段之间不能同时运行时就要设置为互斥的.

使用synchronized锁定多个代码片段,并且指定的同步监视器是同一个时,这些代码片段之间就是互斥的.

package thread;

/**
* 互斥锁
* 当使用synchronized锁定多个不同的代码片段,并且指定的同步监视器对象相同时,
* 这些代码片段之间就是互斥的,即:多个线程不能同时访问这些方法。
*/
public class SyncDemo4 {
public static void main(String[] args) {
Foo foo = new Foo();
Thread t1 = new Thread(){
public void run(){
foo.methodA();
}
};
Thread t2 = new Thread(){
public void run(){
foo.methodB();
}
};
t1.start();
t2.start();
}
}
class Foo{
public synchronized void methodA(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName()+":正在执行A方法...");
Thread.sleep(5000);
System.out.println(t.getName()+":执行A方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void methodB(){
Thread t = Thread.currentThread();
try {
System.out.println(t.getName()+":正在执行B方法...");
Thread.sleep(5000);
System.out.println(t.getName()+":执行B方法完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

死锁

死锁的产生:

两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态。这个现象就是死锁。

package thread;

/**
* 死锁
* 死锁的产生:
* 两个线程各自持有一个锁对象的同时等待对方先释放锁对象,此时会出现僵持状态。
* 这个现象就是死锁。
*/
public class DeadLockDemo {
//定义两个锁对象,"筷子"和"勺"
public static Object chopsticks = new Object();
public static Object spoon = new Object(); public static void main(String[] args) {
Thread np = new Thread(){
public void run(){
System.out.println("北方人开始吃饭.");
System.out.println("北方人去拿筷子...");
synchronized (chopsticks){
System.out.println("北方人拿起了筷子开始吃饭...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println("北方人吃完了饭,去拿勺...");
synchronized (spoon){
System.out.println("北方人拿起了勺子开始喝汤...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println("北方人喝完了汤");
}
System.out.println("北方人放下了勺");
}
System.out.println("北方人放下了筷子,吃饭完毕!");
}
}; Thread sp = new Thread(){
public void run(){
System.out.println("南方人开始吃饭.");
System.out.println("南方人去拿勺...");
synchronized (spoon){
System.out.println("南方人拿起了勺开始喝汤...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println("南方人喝完了汤,去拿筷子...");
synchronized (chopsticks){
System.out.println("南方人拿起了筷子开始吃饭...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println("南方人吃完了饭");
}
System.out.println("南方人放下了筷子");
}
System.out.println("南方人放下了勺,吃饭完毕!");
}
}; np.start();
sp.start(); }
}
package thread;

/**
* 解决死锁:
* 1:尽量避免在持有一个锁的同时去等待持有另一个锁(避免synchronized嵌套)
* 2:当无法避免synchronized嵌套时,就必须保证多个线程锁对象的持有顺序必须一致。
* 即:A线程在持有锁1的过程中去持有锁2时,B线程也要以这样的持有顺序进行。
*/
public class DeadLockDemo2 {
//筷子
private static Object chopsticks = new Object();
//勺
private static Object spoon = new Object(); public static void main(String[] args) {
//北方人
Thread np = new Thread(){
public void run(){
try {
System.out.println("北方人:开始吃饭");
System.out.println("北方人去拿筷子...");
synchronized (chopsticks) {
System.out.println("北方人拿起了筷子,开始吃饭...");
Thread.sleep(5000);
}
System.out.println("北方人吃完了饭,放下了筷子");
System.out.println("北方人去拿勺子...");
synchronized (spoon){
System.out.println("北方人拿起了勺子,开始喝汤...");
Thread.sleep(5000);
}
System.out.println("北方人喝完了汤,北方人放下了勺子");
System.out.println("吃饭完毕。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//南方人
Thread sp = new Thread(){
public void run(){
try {
System.out.println("南方人:开始吃饭");
System.out.println("南方人去拿勺...");
synchronized (spoon) {
System.out.println("南方人拿起了勺,开始喝汤...");
Thread.sleep(5000);
}
System.out.println("南方人喝完了汤,放下勺子...");
System.out.println("南方人去拿筷子...");
synchronized (chopsticks){
System.out.println("南方人拿起了筷子,开始吃饭...");
Thread.sleep(5000);
}
System.out.println("南方人吃完了饭,南方人放下了筷子");
System.out.println("吃饭完毕。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}; np.start();
sp.start();
}
}

day06 Socket_线程API_线程并发安全的更多相关文章

  1. Python 第八篇:异常处理、Socket语法、SocketServer实现多并发、进程和线程、线程锁、GIL、Event、信号量、进程间通讯

    本节内容: 异常处理.Socket语法.SocketServer实现多并发.进程和线程.线程锁.GIL.Event.信号量.进程间通讯.生产者消费者模型.队列Queue.multiprocess实例 ...

  2. 设计模式:基于线程池的并发Visitor模式

    1.前言 第二篇设计模式的文章我们谈谈Visitor模式. 当然,不是简单的列个的demo,我们以电商网站中的购物车功能为背景,使用线程池实现并发的Visitor模式,并聊聊其中的几个关键点. 一,基 ...

  3. python并发编程之线程(一):线程&守护线程&全局解释器锁

      一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍 官网链接:https://docs.pyth ...

  4. java线程安全之并发Queue

    关闭 原 java线程安全之并发Queue(十三) 2017年11月19日 23:40:23 小彬彬~ 阅读数:12092更多 所属专栏: 线程安全    版权声明:本文为博主原创文章,未经博主允许不 ...

  5. python 之 线程池实现并发

    使用线程池实现高IO并发 模块:ThreadPoolExecutor, as_completed 测试代码如下: #!/opt/python3/bin/python3 from concurrent. ...

  6. 性能测试:深入理解线程数,并发量,TPS,看这一篇就够了

    并发数,线程数,吞吐量,每秒事务数(TPS)都是性能测试领域非常关键的数据和指标. 那么他们之间究竟是怎样的一个对应关系和内在联系? 测试时,我们经常容易将线程数等同于表述为并发数,这一表述正确吗? ...

  7. 子进程回收资源两种方式,僵尸进程与孤儿进程,守护进程,进程间数据隔离,进程互斥锁,队列,IPC机制,线程,守护线程,线程池,回调函数add_done_callback,TCP服务端实现并发

    子进程回收资源两种方式 - 1) join让主进程等待子进程结束,并回收子进程资源,主进程再结束并回收资源. - 2) 主进程 “正常结束” ,子进程与主进程一并被回收资源. from multipr ...

  8. python之线程和进程(并发编程)

    python的GIL In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native ...

  9. Python并发编程-线程同步(线程安全)

    Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...

随机推荐

  1. 为什么 Nginx 比 Apache 更厉害?

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 为什么Nginx在处理高并发方面要优于httpd,我们先从两种web服务器的工作原理以及工作模 ...

  2. java高级用法之:绑定CPU的线程Thread-Affinity

    目录 简介 Java Thread Affinity简介 AffinityLock的使用 使用API直接分配CPU 总结 简介 在现代计算机系统中,可以有多个CPU,每个CPU又可以有多核.为了充分利 ...

  3. linux中MySQL主从配置(Django实现主从读写分离)

    一 linux中MySQL主从配置原理(主从分离,主从同步) mysql主从配置的流程大体如图: 1)master会将变动记录到二进制日志里面: 2)master有一个I/O线程将二进制日志发送到sl ...

  4. MPLS基础与工作原理

    MPLS Fundamental History of WAN Protocol 1970年代之前 第一个 WAN 用于将办公室与终端连接到大型机和小型计算机系统. 它是从办公室到数据中心的点对点连接 ...

  5. 203. Remove Linked List Elements - LeetCode

    Question 203. Remove Linked List Elements Solution 题目大意:从链表中删除给定的数 思路:遍历链表,如果该节点的值等于给的数就删除该节点,注意首节点 ...

  6. vue2 使用 swiper 轮播图效果

    第一步.先安装swiper插件 npm install swiper@3.4.1 --save-dev 第二步.组件内引入swiper插件 import Swiper from 'swiper' im ...

  7. Linux下添加MySql组件后报无权限问题解决

    Tomcat日志报错如下: Caused by: java.sql.SQLException: Access denied for user 'root'@'localhost' (using pas ...

  8. Spring bean到底是如何创建的?(上)

    前言 众所周知,spring对于java程序员来说是一个及其重要的后端框架,几乎所有的公司都会使用的框架,而且深受广大面试官的青睐.所以本文就以常见的一个面试题"spring bean的生命 ...

  9. React项目实现导出PDF的功能

    在做web项目中,有时候会遇到pdf导出的需求,现根据之前在公司的React项目中遇到的导出PDF需求,整理一个demo出来. 导出PDF需要用到两个依赖包:html2canvas.jspdf 1.安 ...

  10. 使用pip安装库或执行pip命令时报错解决方案

    初次安装pip后执行安装升级一般不会有问题,但是国外的镜像源下载升级由于网速过慢会进行报错,提示需要升级 pip 或者下载速度很慢最后直接报了错如下图: 这个时候只需要修改镜像源即可,建议修改为永久镜 ...