网络
网卡:计算机接入到网络的最关键的设备,是一个IO设备。(输入,将从互联网外部接受到的数据发给cpu。输出,cpu将数据发送给网卡,网卡发送到互联网目标地址。)
路由器拨号到公网,连接子网和公网,IP协议,分配IP地址
IP地址:当前网络的唯一标识
通过公网来进行不同局域网的主机进行通讯,这时可以把公网看成服务器,每个主机看出客户端
传输方式:
协议:TCP(传输控制协议)、UDP(用户报文协议)
TCP协议
-
客户端和服务器之间建立一个通道,就比如修建了一个大粗管道(这个通道就是TCP),而这个大管道里面有两个小管道,这两个管道负责传输数据。
-
服务器一定与很多个客户端连接,所以就会建立很多管道,那么这些管道怎么被区分呢?不同管道传输的数据为什么不会冲突?
端口号就是一台电脑在建立网络连接时来区分网络连接里传输的数据的编号
这里就引入了端口,建立大通道之前首先要有一对相通的IP地址,为了保证数据不会混乱(传给微信的就是传给微信的,传给虎牙的就是传给虎牙的),就需要在建立管道的出入口处给定一个编号(端口号)
-
在建立网络连接时如何来区分这个连接是唯一的?通过网络四元组,(本机IP、本机端口号、对端IP、对端端口号)
-
每个主机上都有65535个端口
外网(服务器)不能主动向内网(客户端)发请求,但内网可以主动向外网发请求
tcp状态迁移图
三次握手:(建立连接的过程)
1.首先服务器端,必须先开启某一个端口的监听(端口不打开,谁都连不上)
2.由客户端向服务器的监听端口发起一个SYN请求,其中SYN等于一个数字,SYN = x,发送的时候,客户端需要选择一个自己的端口发送该SYN请求,一般来说,客户端选择的端口都是随机的,但是当选择完这个端口以后,就一直使用这个端口传输数据,在该连接没有断开之前,不会换端口。当发送完SYN请求以后,客户端的该端口状态变成了SYN_SENT状态(同步信息发送状态)。(右边红线)
3.当服务器端的监听端口,收到客户端发送的SYN=x这个数据的时候,自己的端口就变成了SYN_RECV,同时向发送SYN的客户端的那个端口回复一个数据,这个数据包含两条信息,一个是SYN=y,另一个是ACK=x+1(回复这个ACK的原因是:每个服务器会收到很多的客户端发来的请求,所以服务器一定要告诉客户端我接收到你发来的这个请求了,所以回复一个x+1,加以确认)(左绿线)
4.当客户端收到服务器发过来的SYN + ACK 消息的时候,我的客户端就会将端口立刻变换成ESTABLISH状态,同时,客户端会发送一条数据给服务器,这个数据是ACK = y+1。
5.当服务器收到客户端发来的ACK = y+1 的时候,服务器中的SYN_RECV状态就变成了ESTABLISH状态。
只有在端口在Establish状态才能传输数据,否则就表示连接没有建立好
四次挥手有
为什么ACK与FIN不一起发送呢?
首先客户端向服务器发送FIN代表我要与你断开了(Client不再向Service发送数据了,①要断了,但并不代表Service不再给Client发送数据了),然后服务器等待把①号流中剩下的流接受完,再向客户端发送ACK,我知道了,我也不再接受你发的消息了,可以断开了(这时Client指向Service的那个流①就可以断开了)。然后服务器知道了要断开了,并且自己要发送的数据也发送完毕了,再向客户端发送一个FIN,意味着服务器不会再发送数据了,客户端收完了FIN后,再返回一个ACK代表我收完了,可以断开了,这时Ⅱ号流就可以断开了。
ACK代表收完了,并不代表发完了,FIN才代表收完了
什么时候才发ACK呢?比如客户端先给服务器发送一条数据,现在发送了八条了,服务器不会向客户端发送ACK,只有收到十条了,才发送ACK
如果FIN和ACK一起发送,那么客户端向服务器发送完FIN后,①号流还不能断开,要等到服务器发送给客户端发送的数据发送完了,①号流才能断开,这样就浪费了资源。
注意:FIN和ACK分开发,至于谁先发过去无所谓,可以是ACK先到,断开①,也可以是FIN先到,代表你把给我发数据了,刚好我也不想给你发了
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
TCP协议属于传输层协议,而我们写代码一般是直接使用封装好的tcp工具或者应用层协议来驱动传输层的工作。如果我们想在JAVA语言中,驱动TCP协议来传输数据,那么我们可以使用JAVA封装好的工具叫做Socket(套接字)编程实现。Java给出的TCP套接字就是Socket,而需要分清楚服务器和客户之间使用的不同,服务器端使用的是ServerSocket,客户端使用的是Socket。
Socket只是一个操作网络传输数据的工具
约定:1024以内的端口都被占中着,给应用层协议分配好了(比如80端口号是预留给了应用层的HTTP协议)
实例:
public class TCPServer {
public static void main(String[] args) {
ServerSocket ss = null;
try {
ss = new ServerSocket(18888);//监听端口号,一般使用10000~60000之间的
//只要程序不断就一直监听这个指定的端口号
Socket s = ss.accept();//接受客户端发来的请求。阻塞式的,客户端不发送他就一直等待着
//s是客户端这头,ss是服务器这头
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
ss.close();//TCP连接断开,关闭流,发送FIN
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class TCPClient {
public static void main(String[] args) {
Socket s = null;
try {
s = new Socket("127.0.0.1",18888);//给出要连接的服务器的IP地址和他的端口号
//客户端的端口号一定是随机的,但是要连接的服务器的端口号一定要知道(只有对端的端口号是开放的才能连接他)
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
s.close();//TCP连接断开,关闭流,发送FIN
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ServerSock不负责传输数据,只负责创建一个可以传输数据的工具的工具
ServerSocket的setSoTimeout(int SoTime)方法:设置超时时间,连接过程中如果超过指定时间还没连接上将会抛出异常。
设置setSoTimeout,解决DDOS攻击(只发SYN不发ACK,占用了全部的端口,让服务器崩溃)
Socket的方法:
getKeepAlive():[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LkJ7BDj1-1582506817635)(C:\Users\张澳琪\AppData\Roaming\Typora\typora-user-images\image-20200215202437921.png)]
isConnected():返回套接字的连接状态
socket还提供两个方法:getInputStream() 和 getOutputStream()
getInputStream() :获得输入流
getOutputStream():获得输出流
结合TCP连接和IO流,完成客户端与服务器的交互(一问一答式)
服务器
/**
* @author ZAQ
* @create 2020-02-11 11:22
*/
public class TCPServer {
public static void main(String[] args) {
ServerSocket ss = null;
try {
//服务器的ServerSocket,只做接入的监听
ss = new ServerSocket(18888);
//接受到一个客户端请求,并生成新的套接字
Socket s = ss.accept();
//获得输出流并将输出流包装成字符流
OutputStream os = s.getOutputStream();
PrintWriter pw = new PrintWriter(os);//字符流,可以一次输一行,封装字节流
//获得输入流并将输入流包装成字符流
InputStream is = s.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//使用输出字符流,输出一句话为客户端
pw.println("欢迎来到我的服务器!");
pw.flush();//字符流输出都要冲刷缓冲区
//创建扫描控制台输入的工具
Scanner scanner = new Scanner(System.in);//Scanner就是对字符流的一个封装,Scanner的作用就是从流里扫描内容
//获得客户端的反馈
String sss = null;
while ((sss = br.readLine()) != null) {
System.out.println(sss);
//接受到内容后再次向客户端发送内容
pw.println(scanner.nextLine());
pw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端
/**
* @author ZAQ
* @create 2020-02-11 11:59
*/
public class TCPClient {
public static void main(String[] args) {
Socket socket = null;
try {
socket = new Socket("127.0.0.1",18888);
InputStream is = socket.getInputStream();
// BufferedReader br = new BufferedReader(new InputStreamReader(is));//法一
// System.out.println(br.readLine());
Scanner scanner = new Scanner(is); //法二 两种方法一样
// System.out.println(scanner.nextLine());
Scanner scanner1 = new Scanner(System.in);
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
String str = null;
while ((str = scanner.nextLine()) != null) {
System.out.println(str);
pw.println(scanner1.nextLine());
pw.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
TCP的特点:
tcp传输数据是需要建立一个有效连接的,也就是说,我们使用Tcp传输数据的第一件事是建立一个数据通道的。
TCP的优点:
- Tcp对于有线连接是有实时感知的,也就是说,Tcp连接中断时,另一方是有能力立刻知道的
- Tcp传输的数据是安全的、有效的、有序的(这是因为TCP连接要建立管道链接)
TCP的缺点:
- Tcp占用资源更多
- Tcp的有序性,一定情况下会拖慢应用层程序的运行
目前市面上见多的很多IM软件(即时通信软件)很多哦都不去采用TCP,而是采用UDP来实现。
UDP协议
- UDP叫做用户报文协议
- UDP是面向数据包的,或者说面向无连接的(不涉及到状态转换)
- UDP传输数据是不可靠的,但传输速度很快
- UDP传播数据的过程中,没有流的概念,只有报文概念
- 对于UDP来说,发送数据使用的是DatagramSocket,接受数据也是使用的DatagramSocket,
所以DatagramSocket没有所谓的明显的服务和客户端的区别(注:Socket有,服务端是ServerSocket,客户端是Socket)
- DatagramSocket是收发数据的工具,但是并不关心数据发送去哪里,或者从哪里来,他只做一件事情,就是要么将数据发到网络当中,让数据自己去自己该去的地方(你去哪我不关心),要么就是从网络里面打捞出来自己要的数据。
- 真正的内容容器是,DatagramPacket,这个报文中在发送端,我们要设置其发送的内容,发送的地址,发送的端口号,等。设置完给DatagramSocker就可以了
- 在接收端,我们要通过DatagramSocket来接收数据,接受的数据在byte[]里面,拿出来就用
- UDP传输数据的过程中,没有流的概念,只有报文的概念。
UDP建立链接发送数据
DatagramPacket的构造器的两个参数
- byte[] buf:接受数据的缓冲区,接受来的数据放这里
- int length:接受数据长度
- length的长度必须小于等于buf数组的长度
服务器
public class UDPServer {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket(9999);
DatagramPacket dp = new DatagramPacket(new byte[128],128);//接受数据的缓冲区
socket.receive(dp);//接收数据到了byte数组中了
byte[] b = dp.getData();//获取这些数据
System.out.println(new String(b));//打印数据
} catch (SocketException e) {
e.printStackTrace();
}
}
}
客户端
public class UDPClient {
public static void main(String[] args) {
try {
DatagramPacket dp = new DatagramPacket("你好UDP".getByte(),"你好UDP".getByte().length());
packet.setAddress(InetAddress.getByName("127.0.0.1"));//设置要发送到的IP地址
packet.setPort(9999);//设置要发送到的端口号
DatagramSocket socket = new DatagramSocket();//这里可以设置发送端的端口号
socket.send(packet);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
}
}
}
有一个问题:接收端DatagramPacket里设置的数组的长度必须随发送端设置的数组长度变化,但是接收端又不知道这个长度是多少,为了解决这一问题,那就约定让发送端每次发送定长数组的数据(比如一个包就发128个字节)
UDP建立连接并且传输数据代码:
服务器
/**
* @author ZAQ
* @create 2020-02-12 20:37
*/
public class UDPServe {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket(9999);
DatagramPacket dp = new DatagramPacket(new byte[128],128);//接收端的数组长度要随发送端的变化
receive(socket,dp);
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void receive(DatagramSocket socket, DatagramPacket dp) throws IOException {
//合并接受到的包
byte[] result = new byte[0];
int num = 0;
while (true) {//收数据的时候不知道具体要收几个包
//这里可以判断收来的数据大小不否小于128,如果比128小了,则就是最后一个包了
socket.receive(dp);
result = Arrays.copyOf(result,result.length + dp.getLength());
System.arraycopy(dp.getData(),0,result,num*128,dp.getLength());
num++;
if(dp.getLength() < 128) {
break;
}
}
System.out.println(new String(result));
}
}
客户端
/**
* @author ZAQ
* @create 2020-02-12 11:48
*/
public class UDPClient {
public static void main(String[] args) {
DatagramPacket packet = null;
DatagramSocket socket = null;
try {
packet = new DatagramPacket(new byte[128],128);
socket = new DatagramSocket();
packet.setAddress(InetAddress.getByName("127.0.0.1"));//ip地址
packet.setPort(9999);//对应ip地址的端口号
Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
send(str,socket,packet);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void send(String source, DatagramSocket socket, DatagramPacket packet) throws IOException {
//用于存输入的数据
byte[] arrays = source.getBytes();//arrays里就是要发送的数据
int num = arrays.length / 128; //计算要发送多少次包
byte[] datas = null;
//人工拆包
for(int i = 0; i <= num; i++) {
//最后一包,这时数据长度不一定是128
if(i == num) {
datas = Arrays.copyOfRange(arrays,i*128,arrays.length);
packet.setData(datas);
packet.setLength(datas.length);//修改接受长度
} else {
//没到头的数据包
datas = Arrays.copyOfRange(arrays,i*128,(i+1)*128);
packet.setData(datas);
//长度就是128,不用改
}
socket.send(packet);
}
}
}