赞
踩
Future:表示将要完成的任务的结果
PS:Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callable一边在任务执行成功或失败之后做出相应操作
主要方法:
cancel: 取消Callable的执行【在Callable没有执行完毕之前】
get: 获取Callable的返回值
isCanceled : 判断是否被取消了
isDone: 判断是否完成了
同步执行和异步执行
同步执行:同步调用的时候回等待响应或返回值,若没有响应或返回值,就会阻塞线程,不会继续执行
异步执行:异步调用就是单独的进行一个线程执行,原始线程启动异步调用,异步会调用另外一个线程执行请求,原始线程不会等待这个异步线程,彼此之间同时执行,只需要异步线程执行完毕之后通知原始线程即可
需求:
某一天早上,你去吃早点,点包子(需要3分钟)和凉菜(需要1分钟),如果此时一个串行的执行【同步执行】,要吃上早点需要4分钟
但是你在等待包子的时候用,其他人【服务员】可以准备凉菜,或者可以两种同时准备,最常时间就3分钟【这就是异步执行】,而Future就是这种执行模式
非Future模式
package com.qfedu.Future;
//同步执行某一天早上,你去吃早点,点包子(需要3分钟)和凉菜(需要1分钟),如果此时一个串行的执行【同步执行】
public class SynchronizedThreadDemo {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
//开启两个线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("包子准备完毕了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("凉菜准备完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//在学习线程方法时候,有一个方法必然让出cpu时间片,等待其他线程执行完毕之后在继续执行 join
//必须在start方法之后调用
t1.start();
t1.join();
t2.start();
t2.join();
long end = System.currentTimeMillis();
System.out.println("准备的时间是:"+(end-start));
}
}
Future模式
package com.qfedu.Future;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//但是你在等待包子的时候用,其他人【服务员】可以准备凉菜,或者可以两种同时准备,最常时间就3分钟【这就是异步执行】
public class FutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
//创建两个线程进行当前菜品的准备
//1.创建两个Callable接口
//凉菜
Callable<String> callable1 = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "凉菜准备好了";
}
};
FutureTask<String> stringFutureTask = new FutureTask<>(callable1);
//这一步操作是借用Thread线程执行Callable接口中线程实现【线程执行的逻辑】,最终的处理结果会回到FutureTask对象中
new Thread(stringFutureTask).start();
//包子
Callable<String> callable2 = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(3000);
return "包子准备好了";
}
};
//因为Thread并没有一共当前Callable接口的参数作为构造方法,所以需要对Callable进行一个转换
//需要使用到另外一种形式,使用FutureTask,因为FutureTask实现Runnable,所以可以作为参数进行传递
//FutureTask泛型是受Callable接口泛型影响
FutureTask<String> stringFutureTask2 = new FutureTask<>(callable2);
//这一步操作是借用Thread线程执行Callable接口中线程实现【线程执行的逻辑】,最终的处理结果会回到FutureTask对象中
new Thread(stringFutureTask2).start();
//FutureTask中对象如何获取值,在这里可以使用 get方法
System.out.println(stringFutureTask.get());
System.out.println(stringFutureTask2.get());
long end = System.currentTimeMillis();
System.out.println("准备时间:"+(end-start));
}
}
PS:"Future多用于在线程池中,配合Callable接口使用,单独创建Thread类来计算Callable这样方式相对较少"
练习:使用线程池,使用两个线程并发机选150,51100的和,然后在汇总
package com.qfedu.Future;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
//Future使用
public class FutureThreadPoolDeom {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池对象的时候,我们多使用于ExecutorService作为当前对象类型【submit 和 shutdown】
//创建线程池的时候需要使用到工具类 Executors 这个工具类提供4个线程池的创建方式
ExecutorService executorService = Executors.newFixedThreadPool(2);
//创建任务提交到线程池中进行计算【在提交任务之前,需要创建任务】
//线程池能接收任务的两种形态 : 一种 Runnable【不需要计算结果选择它】 另外一种Callable【需要计算结果选择它】
//Future 还是 FutureTask 都支持get方法,所以都可以获取数据
Future<Integer> submit = executorService.submit(() -> {
int sum = 0;
for (int i = 1; i <= 50; i++) {
sum += i;
}
System.out.println("1-50就算完毕");
return sum;
});
Future<Integer> submit1 = executorService.submit(() -> {
int sum = 0;
for (int i = 51; i <= 100; i++) {
sum += i;
}
System.out.println("51-100就算完毕");
return sum;
});
//获取结果值计算
int sum = submit.get() + submit1.get();
System.out.println("结果集:"+sum);
//线程池使用个完毕之后,就关闭,不建议长期开启,但是如果是部署服务器端,【定期关闭】,千万别用完就关闭
//线程池关闭之后就不允许在接收任务
executorService.shutdown();
}
}
这个锁是支持一写多读的同步锁,读写分离,可以分别配置读锁、写锁,支持多次分段读锁,使多个读操作可以并发执行
Ps:这个锁主要是应对读取和写入操作【对某个变量的操作】,这个锁需要在一个特定的情况下使用效果是最好【读的次数多,写的次数少】
互斥原则【会影响效率】
写-写:互斥,阻塞【两个线程操作写操作】
读-写:互斥,读阻塞写,写阻塞读【两个线程操作读和写】
读-读:不互斥,不阻塞【两个线程同时读取,效率高】
PS:这个锁是在**【读操作远远高于写操作】**的情况使用,可在保障线程安全的情况下,提高效率
"这个锁的使用和互斥锁【Lock】是一样,依旧需要使用 lock上锁 unlock释放锁"
"这个锁提供了两个锁对象一个 read【读锁】 write【写锁】"
package com.qfedu.ReentrantReadWriterLock;
import java.util.concurrent.TransferQueue;
import java.util.concurrent.locks.*;
//演示读写锁使用
public class ReadWriteDemo {
//1.先创建读写锁对象
private ReentrantReadWriteLock rrl = new ReentrantReadWriteLock();
//2.分别获取读锁和写锁
private ReentrantReadWriteLock.ReadLock read = rrl.readLock();//读锁
private ReentrantReadWriteLock.WriteLock write = rrl.writeLock();//写锁
private ReentrantLock lock = new ReentrantLock();
private String value;//模拟数据
// 读写操作 需要效率 建议使用方式 ReentrantReadWriteLock > Lock > synchronized
//读取数据
public String getValue(){
//即想保证速度快,也想保证线程安全
//read.lock();
lock.lock();
try {
Thread.sleep(1000);
System.out.println("读取:"+value);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//read.unlock();
lock.unlock();
}
return value;
}
//写入
public void setValue(String value){
//即想保证速度快,也想保证线程安全
//write.lock();
lock.lock();
try {
Thread.sleep(1000);
System.out.println("写入:"+value);
this.value = value;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//write.unlock();
lock.unlock();
}
}
}
package com.qfedu.ReentrantReadWriterLock;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//读写锁性能测试
public class ReentrantReadWriteLockTimeDmeo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
//创建带有锁类的对象,通过线程进行操作
ReadWriteDemo rwd = new ReadWriteDemo();
Runnable read = new Runnable() {//线程读取
@Override
public void run() {
rwd.getValue();
}
};
Runnable write = new Runnable() {//线程读取
@Override
public void run() {
rwd.setValue("李四"+new Random().nextInt(100));
}
};
long start = System.currentTimeMillis();
for(int i = 0;i<2;i++){
//分配两个线程写
executorService.submit(write);
}
for (int i = 0;i<18;i++){
//分配18个线程读取
executorService.submit(read);
}
executorService.shutdown();
//保证线程执行都执行完毕,正常关闭 true就是正常关闭 false没关闭
while( !executorService.isTerminated()){}
//在执行其他操作
long end = System.currentTimeMillis();
// System.out.println("使用读写锁:"+(end-start));//4秒
System.out.println("使用互斥锁:"+(end-start));//20秒
}
}
线程安全的类或接口,这个包也提供了一套在多线程并发访问下,线程安全的集合
PS:在图中以蓝色标识的集合都是线程安全的,都是Java8中所提供
在Java5之前,在多线程并发访问集合,要使用线程安全的集合需要使用Collections工具类中synchronized方法进行集合转换,才会得到线程安全集合【虽然是线程安全的,但是效率低(synchronized修饰)】,而在Java5之后提供了一个并发包【java.util.concurrent】,在这个并发包中它提供一系列安全集合【不仅线程安全,效率也高】
Collections工具类提供线程安全转换方式【线程安全集合】
static <T> Collection<T> | synchronizedCollection(Collection<T> c) 返回由指定集合支持的同步(线程安全)集合。 |
---|---|
static <T> List<T> | synchronizedList(List<T> list) 返回由指定列表支持的同步(线程安全)列表。 |
static <K,V> Map<K,V> | synchronizedMap(Map<K,V> m) 返回由指定的Map支持的同步(线程安全)Map。 |
static <K,V> NavigableMap<K,V> | synchronizedNavigableMap(NavigableMap<K,V> m) 返回指定的导航Map支持的同步(线程安全)导航Map。 |
static <T> NavigableSet<T> | synchronizedNavigableSet(NavigableSet<T> s) 返回由指定的导航集支持的同步(线程安全)导航集。 |
static <T> Set<T> | synchronizedSet(Set<T> s) 返回一个由指定集合支持的同步(线程安全)集。 |
static <K,V> SortedMap<K,V> | synchronizedSortedMap(SortedMap<K,V> m) 返回一个由指定的排序映射支持的同步(线程安全)排序的Map。 |
static <T> SortedSet<T> | synchronizedSortedSet(SortedSet<T> s) 返回一个由指定的排序集支持的同步(线程安全)排序集。 |
线程安全问题
package com.qfedu.ConcurrentCollection;
import java.util.ArrayList;
//多线程并发访问线程不安全集合的问题
public class Demo {
public static void main(String[] args) {
//在单线程的前提下,效率高【线程不安全】
ArrayList<String > list = new ArrayList<>();
//使用线程对ArrayList集合进行修改
//使用多线程进行修改
for (int i = 0;i<20;i++){//相当于开始20个线程
int tmp = i;
new Thread(()->{
for(int j = 0;j<5;j++) {
//一个线程对集合进行5次赋值
//ConcurrentModificationException 并发修改异常【多线同时修改】
list.add(Thread.currentThread().getName() + "====" + tmp + "====" + j);
}
}).start();
System.out.println(list);
}
}
}
常规解决方案:使用Collections工具类对ArrayList集合转变为线程安全的结合
修改代码
package com.qfedu.ConcurrentCollection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
//因为是多线程并发访问,所以可以使用Collections工具将集合转变为线程安全
public class JDK_5ChangeCollection {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
//将当前集合转变为线程安全集合
List<String> synList = Collections.synchronizedList(list);
//使用多线程进行修改
for (int i = 0;i<20;i++){//相当于开始20个线程
int tmp = i;
new Thread(()->{
for(int j = 0;j<5;j++) {
//一个线程对集合进行5次赋值
synList.add(Thread.currentThread().getName() + "====" + tmp + "====" + j);
}
}).start();
System.out.println(synList);
}
}
}
CopyOnWriteArrayList线程安全List集合
特点:
线程安全的ArrayList集合,加强版本的读写分离
写是有锁,读数据是无锁,读写之间不阻塞,性能优于读写锁
写入数据时,先copy一个容器的副本,在添加新元素,然后替换引用【会影响一定量的性能】
【它的使用和ArraysList无任何差别】
PS:这个集合又一个小缺点以空间换安全,因为写入数据时候需要copy一个副本然后然后重新替换引用,这个类的内部实现使用Lock锁作为同步锁
"在出现多线程并发访问ArrayList集合的时候,可以不使用Collections工具类在进行转换"
"直接创建CopyOnWriteArrayList对象,这个对象同时兼容ArrayList集合操作方式"
CopyOnWriteArrayList list = new CopyOnWriteArrayList() --> "依然支持泛型"
CopyOnWriteArraySet线程安全Set集合
说明:
线程安全的set集合,底层的实现是使用CopyOnWriteArrayList实现
唯一不同在于,使用addIfAbsent方法元素【元素存在不添加,元素不存在添加】,遍历数据组【间接的造成新能下降】
PS:这个集合是排重,它的排重原则不是Hash表,而是equals【只要连个元素相等,就认为元素出现了,所以会不存储
【CopyOnWriteArraySet操作方式和HashSet无异】
演示
"在出现多线程并发访问HashSet集合的时候,可以不使用Collections工具类在进行转换"
"直接创建CopyOnWriteArraySet对象,这个对象同时兼容Hashset集合操作方式"
CopyOnWriteArraySet set = new CopyOnWriteArraySet() --> "依然支持泛型"
ConcurrentHashMap线程安全Map集合[必须的]
【ConcurrentHashMap的操作和HashMap无异】
Java7:
1.创建一个出是容量为16的Segment【段】,是同分段锁设计
2.不是对整个Map进行加锁,是对每个Segment加锁
3.当多个对象存入同一个Segment的时候,才会出现互斥
4.理想状态16对象存储时无重复,这样并行度高【16个线程一起执行,效率最大值】
Java8:
1.抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发安全,采用全新的存储方式:【数组+链表+红黑树】
2.CAS机制三个核心参数【V、E 、N】 V:要更新变量 E:预期值, N:新值
3.只有当 V == E时,V值才会进行新值的更新 即 V = N,【表示当前数据已经更新了】, 则取消操作
PS:CAS算法是硬件内嵌支持,计算机硬件就已经完成同步
"在出现多线程并发访问HashMap集合的时候,可以不使用Collections工具类在进行转换"
"直接创建ConcurrentHashMap对象,这个对象同时兼容HashMap集合操作方式"
ConcurrentHashMap map = new ConcurrentHashMap() --> "依然支持泛型"
Collection的子接口表示队列FIFO(先进先出)
这个的方法可以参考LinkedList,如果要添加元素
offer(元素)添加一个元素 poll 获取第一个元素并移除 peek 获取第一个元素取但不移出
ConcurrentLinkedQueue 线程全的Queue集合
说明:线程安全,可以高效读写的读写的对垒,高并发下是性能最好的队列
PS:这个队列使用CAS算法
演示:
ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); "依就支持泛型"
BlockingQueue接口(阻塞队列)
这个队列的方式有点类似于【生产者消费者模型】
这个队列有两个实现类:
ArrayBlockingQueue 数组结构实现,有界队列【存储范围是固定了】
BlockingQueue bq = new ArrayBlockingQueue(10);
PS:"这里指的有界是指当前创建队列集合的时候就指定集合中可以存储多少个数据,这个存储数个数是通过构造方法参数决定"
"上面和这个对象就相当于只能存储10个对象"
LinkedBlockingQueue 链表结构实现,有界队列
BlockingQueue bq = new LinkedBlockingQueue();
PS:"如果这个有界队列不该参数, 会使用默认值 [Integer.MAX_VALUE]"
PS: ArrayBlockingQueue创建是必须给大小的而LinkedBlockingQueue可以给也可以
演示
package com.qfedu.ConcurrentCollection;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<Object> objects = new ArrayBlockingQueue<>(6);
//LinkedBlockingQueue<Object> objects1 = new LinkedBlockingQueue<>();
new Thread(()->{
for (int i = 0;i<30;i++){
try {
objects.put(i);//生产
System.out.println(Thread.currentThread().getName()+"生产面包"+i+"个数");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"花花").start();
new Thread(()->{
for (int i = 0;i<30;i++){
try {
Object take = objects.take();//消费
System.out.println(Thread.currentThread().getName()+"消费面包"+take+"个数");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"双双").start();
}
}
Java是Internt上语言,它从语言级别上提供了对网络应用程序的支持,程序猿能够开发常见网络应用程序**[聊天]**
Java提供了网络类库,可以实现不同的网络连接,联网的底层实现被隐藏 Java的内部,我们只需要调用其提供方法即可实现网络编程
网络基础
什么是计算机网络?
把分布在不同地理区域的计算机与专门的外部设备用通信线路互相连成一个规模大,功能强大网络系统,从而可以使众多计算机可以方便的互相传递信息,共享硬件,软件和数据信息等资源。
网络编程的目的:
直接或间接的通过网络写与其他计算机进行通信
网络编程有两个主要的问题:
1.如何准确的定位网络上一台或多台主机【IP】
2.找到注解后如何高效传输数据【流的选择】
PS:由点和线组成,表示诸多对象间的互相联系,这个就是网络
联网设备之间可以更好进行交互,而要遵守的一套规则。
通信协议有两套
1.OSI模型【网络7层模型】,过于理想化,未能在网络进行广泛的推广
2.在7层协议的基础之上,进行修改推出了一个4层协议【TCP/IP模型】
我们主要研究就是4层协议中TCP/IP这一层【传输层和网络层】
物理层和数据链路层涉及物理介质访问和二进制数据流传输。
网络层的主要协议有IP(Internet protocol)、
ICMP(Internet Control Message Protocol,互联网控制报文协议)、
IGMP(Internet Group Management Protocol,互联网组管理协议)、
ARP(Address Resolution(睿哲鹿神) Protocol,地址解析协议)
RARP(Reverse(瑞我司) Address Resolution Protocol,反向地址解析协议)等。
涉及寻址和路由选择
传输层的基本功能是为两台主机间的应用程序提供端到端的通信。传输层从应用层接受数据,并且在必要的时候把它分成较小的单元,传递给网络层,并确保到达对方的各段信息正确无误。
应用层提供应用程序的网络接口。
我么现在主要研究的是
传输层 TCP UDP
网络层 IP(IPv4,IPv6)
IP协议 Internet Protocol Address 互联网协议地址/网络协议地址【分配给互联网设备的数字标签(唯一标识符)】
IP地址种类
1.IPV4:4字节的32位整数,并分为4段8位的二进制数据,每8位之间用圆点隔开,每8位整数可以转为一个【0~255】十进制整数
格式:D.D.D.D --》翻译 --》 192.168.9.101
**2.IPV6:**16字节的128位整数,并分成8段十六进行数,每16位之间用圆点隔开,每16位整数会可以转换为一个【0~65535】十进制整数
格式:X.X.X.X.X.X.X.X --》翻译 --》 fdb2:2c26:f4e4:0:3421:943d:8040:8eef
PS:在各位电脑没有连接任何网络的前提现,自身电脑存在一个环形网络,这个网络的IP地址就是 127.0.0.1【localhost】
IPV4地址的5种分类
A类保留给政府机关的 1.0.0.1 ~126.255.255.254
B类分配中等公司使用 128.0.0.1~191.255.255.254
C类分配给个人使用 192.0.0.1 ~ 223.255.255.254
D类用于组播 224.0.0.1 ~ 239.255.255.254
E类用于实验室 240.0.0.1~ 255.255.255.254
ps:现在的世界已经没有真正 IP地址了【IPV4地址划分完毕】。现阶段推荐就是IPV6地址
端口号
在通信实体上进行网络通信的唯一标识
端口号分类:
公认端口号: 0~1023
注册端口号:1024~49151
动态或私有端口: 49152 ~ 65535
PS:编写代码的时候,端口号的范围就是从0~65535之间 ,但是这里从0~1024 不要使用这些被系统程序占用,开发时端口号越奇葩越好
常用端口:MySql: 3306 Oracle:1521 Tomcat:8080
IP地址类,在Java中可以以当前类作为IP地址的一个抽象,这个类有两个子类【Inet4Address,Inet6Address】
InetAddress类对象含有一个Internet主机地址【这个主机地址可以是一个域名(www.baidu.com)或IP地址(192.168.9.101)】
PS:我们平时上网的时候是不会记录IP地址,而我们记录的是一个域名(主机的域名就是一个IP地址的映射),网络中就提供了一个域名服务器【DNS】负责将域名转换为IP地址,这样就可以和主机建立连接
DNS1 114.114.114.114
DNS2 8.8.8.8
演示:
package com.qfedu.InteAddress;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
//IP地址的抽象类
public class InetAddressDemo {
public static void main(String[] args) throws Exception {
//局域网内部的IP地址对象
//1.使用本机IP地址创建IP对象【IP地址对象组成(主机名+IP地址)】
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
//2.通过IP地址对象获取IP地址
System.out.println("ip地址:"+localHost.getHostAddress());
//3.通过IP地址对象获取主机
System.out.println("主机:"+localHost.getHostName());
//4.通过IP地址创建IP对象 参数是一个IP地址字符串
InetAddress byName = InetAddress.getByName("10.211.55.3");
//可以传入本机名字,本机地址
InetAddress localhost = InetAddress.getByName("localhost");
//自己电脑其实有一个内部网络【内部IP 127.0.0.1 --》 localhost】
//需要获取百度的IP地址,同样可以使用getByName方法 还可以解析域名
InetAddress byName1 = InetAddress.getByName("www.baidu.com");
//获取当前IP地址
System.out.println("www.baidu.com的IP地址是:"+byName1.getHostAddress());
//判断当前访问是否超时?【设置一个时间参数(毫秒),只要在规定时间内访问到这返回true,访问不到返回false】
System.out.println("2秒内是否可以访问?"+byName1.isReachable(2000));
//获取IP地址对象中的所有信息
InetAddress[] allByName = InetAddress.getAllByName("www.baidu.com");
for(InetAddress ip : allByName){
System.out.println(ip.getHostAddress());
}
}
}
URI、URL和URN的区别
拿人做例子,假设这个世界上所有人的名字都不能重复,那么名字就是URI的一个实例,通过名字这个字符串就可以标识出唯一的一个人。
现实当中名字当然是会重复的,所以身份证号才是URI,通过身份证号能让我们能且仅能确定一个人。
那统一资源定位符URL是什么呢。也拿人做例子然后跟HTTP的URL做类比,就可以有:
动物住址协议://地球/中国/浙江省/杭州市/西湖区/某大学/14号宿舍楼/525号寝 /张三.人
https://hao.360.com/?a1004
可以看到,这个字符串同样标识出了唯一的一个人,起到了URI的作用,所以URL是URI的子集。URL是以描述人的位置来唯一确定一个人的。
在上文我们用身份证号也可以唯一确定一个人。对于这个在杭州的张三,我们也可以用:
身份证号:123456789来标识他。
所以不论是用定位的方式还是用编号的方式,我们都可以唯一确定一个人,都是URl的一种实现,而URL就是用定位的方式实现的URI。
URN,统一资源命名,是通过名字来标识资源,比如mailto:java-net@java.sun.com。
TCP/IP是一个网络通信协议,网络通信协议就是一个约定,即通过这个约定可以达到,对速率、传输代码、代码结构、传输控制、出错控制等等
现阶段通信协议是4层【网络接口层,数据层,传输层,应用层】,当前我们要处理得到就是数据和传输层
传输层的协议中有两个非常重要的协议:
传输控制协议TCP(Transmission Control Protocol)
用户数据报(报文)协议UDP(User Datagarm Protocol)
TCP/IP以其两个主要协议:传输控制协议和互联网协议而得名,实际上是一组协议,我实际开发TCP程序的时候,习惯就把IP跟在TCP的后面,就形成了TCP/IP.
IP(Internet Protocol)网络层的主要协议,支持网络之间互相连接和通信
TCP协议
使用TCP协议之前,需要先建议TCP连接,形成传输数据通道
传输数据前,需要执行**【三次握手】**才能建立连接实现通信
TCP是一个点对点的传输,TCP通信协议中有两个应用进程:【客户端】 和 【服务端】
建立连接成功之后可以进行大量数据传输
传输完毕之后,需要释放连接需要执行**【四次挥手】**,挥手成功之后才会关闭连接
特点:TCP是一种面向连接、可靠的、基于字节流的传输通信协议,数据大小无限制,建立连接时候需要有三次握手,断开连接时候需要有四次挥手
TCP报文中重要标识
1.序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据是对此进行标记
2.确认序号: ack序号,占32位,只有ack标志位1时,确认序号字段才有效【ack = Seq+1】
3.TCP报文中有6个标志位即【URG、ACK、PSH、RST、SYN、FIN】
URG:紧急指针有效
ACK:确认序号有效
PSH:接收方应该尽快经这个报文提交给应用层
RST:重置连接
SYN:发起一个新连接
FIN:释放一个连接
PS:不要将确认需要ack与标志位中ACK搞混, 确认学号ack = 发起方Seq+1 (报文中ACK【大写】表示一个标志位)
TCP建立连接(三次握手)
所谓的三次握手即建立TCP连接,就是只建立一个TCP连接时,所需要客户端和服务端总共发送3个包确认建立连接
(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
三次握手【通俗版本】:客户端向服务器发起请求,服务器响客户端请求,并返回消息给客户端,客户端接收服务器返回消息,确认服务器可以连接,此时连接建立成功
例如:打电话模拟三次握手
1.拿起电话拨号【客户端向服务器发起请求】
2.对方拿起电话说“喂”【服务器响应客户端请求,并返回确认信息】
3.双方法开始对话【“连接建立成功开始传输数据“】
TCP断开连接(四次挥手)
PS:这个连接断开可能是双向的【可能是Client端断开】 或【可能是Server端断开】
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
四次挥手【通俗版本】:客户端向服务器发送关闭请求,服务器确认客户端关闭请求,并通知客户端自身也关闭,客户端收到服务器关闭信息, 确认自身关闭不在接收任何数据,此时会再次发送确认关闭信息给服务器,服务器再次接收到关闭信息,进行关闭
例如: 挂电话
1.我这边已经说完了,你那边是否还有什么话要说?【客户端向服务器发起关闭请求】
2.我在想想,你先别急【服务响应请求,客户端等待回应】
3.我这边也没什么事情了,先这个样吧,再见【服务器响应客户端请求,发送确认关闭信息】
4.好的,再见,挂电话。。。。。。【客户端接收信息确认关闭,服务器无法和客户端通信,随之关闭】
Java中提供一个类,这个类是专门用来编写TCP程序使用,这个类叫做Socket【套接字】,如果需要完成TCP编程,需要有**【客户端】和【服务端】**,Java中提供Socket类是不能同时代表客户端和服务器,这里的Socket代表的【客户端】,服务器端使用是ServerSocket代表的就是【服务端】
PS:程序的执行一定是先开服务器在开客户端
版本1:
"需求:客户端向服务器发起请求,并传递对服务问好"
package com.qfedu.Scoket.Version_1;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
//客户端
public class Client {
public static void main(String[] args) throws IOException {
//1.创建客户端对象,需要使用Socket对象【它所代表的是客户端】
//参数是一个字符串的IP地址, 另外一个是端口号【不要写1024以内,最大不要超过65535】
Socket client = new Socket("10.211.55.3",8888);
System.out.println(client);
//此时是需要客户端向服务器发送数据【从这端将数据输出到另外一端】
//Socket支持字节流【支持网络字节输入输出流】
//获取输出流对象【建立当前客户端到服务器传输数据的通道】
OutputStream os = client.getOutputStream();
os.write("服务器你好,我是客户端,来之远方的问候!!! 。。。。嘿嘿嘿o(* ̄︶ ̄*)o".getBytes());
//一定要刷新,防止数据在网络中阻塞
os.flush();
//3.关闭客户端
client.close();
}
}
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
//服务器
public class Server {
public static void main(String[] args) throws IOException {
//1.先创建服务器对象
//参数是对应客户端的 端口号,需要连接那些客户端,那么就写哪个端口号
ServerSocket server = new ServerSocket(8888);
System.out.println("服务器等待客户端的连接.........");
//需要在服务器端的内部获取到,连接服务器的客户端实例,在服务器的内部操作这个连接客户端
Socket clinet = server.accept();
System.out.println("恭喜! 来自客户端"+clinet+"已经连接成功.....");
//获取传输的数据,相当于是输入流
InputStream is = clinet.getInputStream();
byte[] bs = new byte[1024];
int len = is.read(bs);
System.out.println("来之客户端的信息:"+new String(bs,0,len));
//关闭服务器
server.close();
}
}
版本2:
"需求:服务器已经接收到了客户端的信息,服务器予以相应【收到信息,感谢访问!】"
package com.qfedu.Scoket.Version_2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
//客户端
public class Client {
public static void main(String[] args) throws IOException {
Socket client = new Socket("10.211.55.3",7777);
System.out.println("客户端信息:"+client);
//1.创建客户端到服务器的数据通道
//1.1 输出流
OutputStream outputStream = client.getOutputStream();
outputStream.write("服务器你好,我是客户端,再次问候你!!!o(* ̄︶ ̄*)o".getBytes());
outputStream.flush();
//1.2输入流【读取从服务器发送来的数据】
InputStream inputStream = client.getInputStream();
byte[] bs = new byte[1024];
int len = inputStream.read(bs);
System.out.println("来自服务器的反馈:"+new String(bs,0,len));
client.close();
}
}
package com.qfedu.Scoket.Version_2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
//服务器
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(7777);
System.out.println("服务器等待客户端的连接.......");
//获取连接服务器端的客户端对象
Socket clinet = server.accept();
System.out.println("恭喜!来自客户端"+clinet+"已经连接上了.........");
//创建服务器到客户端的数据通道
//1.输入流【读取客户端发送给服务器的数据】
InputStream inputStream = clinet.getInputStream();
byte[] bs = new byte[1024];
int len = inputStream.read(bs);
System.out.println("来自客户端的信息:"+new String(bs,0,len));
//2.输出流【服务器向客户端反馈信息】
OutputStream outputStream = clinet.getOutputStream();
outputStream.write("以收到信息,感谢访问!(#^.^#)".getBytes());
outputStream.flush();
server.close();
}
}
版本3:
"需求:可以完成客户端对服务之间的聊天程序"
package com.qfedu.Scoket.Version_3;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//创建客户端
public class Client {
public static void main(String[] args) throws IOException {
//1.创建客户端对象
Socket client = new Socket("10.211.55.3",6666);
//2.创建客户端和服务器数据通道
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
//3.获取控制台输出数据传输给服务器
Scanner input = new Scanner(System.in);
//4.聊天[约定输入886 结束聊天]
while(true){
System.out.println("客户端对服务器说:");
String content = input.nextLine();
outputStream.write(content.getBytes());//客户端发送给服务器的语句
outputStream.flush();
//当输入值是886时停止聊天
if("886".equals(content)){
break;
}
//接收服务器的反馈
byte[] bs = new byte[1024];
int len = inputStream.read(bs);
System.out.println("来自服务器的信息:"+new String(bs,0,len));
}
client.close();
}
}
package com.qfedu.Scoket.Version_3;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
//服务器端
public class Server {
public static void main(String[] args) throws IOException {
//1.创建服务器对象
ServerSocket server = new ServerSocket(6666);
System.out.println("等待客户端连接...... ");
//2.获取连接服务器的客户端对象
Socket client = server.accept();
//3.创建数据通道
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
//4.获取控制台数据
Scanner inuput = new Scanner(System.in);
while(true){
//1.先读取客户端发送来数据
byte[] bs = new byte[1024];
int len = inputStream.read(bs);
String content = new String(bs,0,len);
System.out.println("来自客户端从信息:"+content);
//2.判断输入信息是否需要机继续聊天
if("886".equals(content)){
break;
}
//3.将控制台上述输出给客户端
System.out.println("服务器对客端说:");
String str = inuput.nextLine();
outputStream.write(str.getBytes());
outputStream.flush();
}
server.close();
}
}
PS:需求1【服务器】对多【客户端】
"只需要修改服务器端代码即可,使用线程接受每一个客户端对象"
//服务器端
public class Server {
public static void main(String[] args) throws IOException {
//1.创建服务器对象
ServerSocket server = new ServerSocket(6666);
System.out.println("等待客户端连接...... ");
//2.添加一个死损坏,一次可以接入多个客户端
while(true){
Socket client = server.accept();
new ServerThread(client).start();
}
}
}
package com.qfedu.Scoket.Version_3_1;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
//服务器接收多个客户端的线程
public class ServerThread extends Thread{
//需要在类中创建一个客户端对象属性,用来接收客户端以便操作
private Socket client;
public ServerThread(Socket client) {
this.client = client;
}
//线程中处理的就是整个聊天程序
@Override
public void run() {
try {
//1.创建数据通道
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
//2.获取控制台数据
Scanner inuput = new Scanner(System.in);
while (true) {
//1.先读取客户端发送来数据
byte[] bs = new byte[1024];
int len = inputStream.read(bs);
String content = new String(bs, 0, len);
System.out.println("来自客户端从信息:" + content);
//2.判断输入信息是否需要机继续聊天
if ("886".equals(content)) {
break;
}
//3.将控制台上述输出给客户端
System.out.println("服务器对客端说:");
String str = inuput.nextLine();
outputStream.write(str.getBytes());
outputStream.flush();
}
//整个联系的切断是切断客户端
client.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
ps:服务器一般是不会关闭的,服务器是否永久不关闭?不是,服务器关闭是有时间段,每年的例行维护【后半夜从凌晨2点到4点】
Java中实现UDP的类是DatagramSocket和DatagramPacket【UDP中数据包】
UDP数据报【文】,它通过数据报套接字DatagramSocket进行发送和接收,需要注意系统不保证UDP数据包一定能安全的送到目的地,也不能什么时候发送到【MD5,Base64】
当前在UDP中发送数据会被封装到DatagramPacket中,数据包的大小要小于等于64K
ps:UDP开发客户端即是服务器,服务器即是客户端,UDP的发送和接收形式类似于广播【只有接收和发送端口号一致就可以收到信息】
发送:
package com.qfedu.DatagramScoket;
import java.io.IOException;
import java.net.*;
//UDP发送
public class Send {
public static void main(String[] args) throws IOException {
//1.创建UDP对象
DatagramSocket sender = new DatagramSocket();
//2.书写信息
String content = "呵呵o(* ̄︶ ̄*)o哈哈^_^嘻嘻(#^.^#)嘿嘿(*^▽^*)";
//3.将数据写入到数据包中
DatagramPacket pkt = new DatagramPacket(
content.getBytes()//数据的字节数组
,content.getBytes().length//存储数据的字节数组长度
,InetAddress.getByName("127.0.0.1")//IP地址对象即发送到哪个IP地址下
,9999//端口号
);
//4.循环发送
while(true){
sender.send(pkt);
}
}
}
接收:
package com.qfedu.DatagramScoket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//接收端
public class Receiver {
public static void main(String[] args) throws IOException {
//1.创建UDP接收对象
DatagramSocket receiver = new DatagramSocket(9999);
//2.接收数据包
byte[] bs = new byte[1024];
//2.1将字节数组封装成数据包用于接收
DatagramPacket pkt = new DatagramPacket(bs,bs.length);
receiver.receive(pkt);//接收数据将数据存储到包中
//3.需要对数据包进行解包超作
/*
getData 获取数据包中存储的数据,返回的是一个byte类型数组
getLength 获取数据包中实际存储数据的长度
*/
String str = new String(pkt.getData(),0,pkt.getLength());
System.out.println("接收信息:"+str);
receiver.close();
}
}
练习:
1.通过TCP从工程中读取一张图片,发送到服务器中保存
工程中:dir文件夹存一张图片
img文件从客户端发送图片存储到img文件夹中【服务器中文件夹】
2.使用Socket编程实现服务器端注册
注册信息保存在properties文件中
格式
下面这些信息都是外部控制台输入id:“1001”,name:“tom”,pwd:“123”
然后将数据拼接成字符串【id={id:“1001”,name:“tom”,pwd:“123”}】 传递给服务器
服务器进行解析,将会解析的数据写入到Properties文件中
注册成功过之后返回“注册成功”
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。