计算机网络
1.网络概念
1.计算机网络 通过网络介质 把不同地理位置的计算机连接到一起
数据传输,除了网络还有一系列协议的参与。双方要传输的数据最终会转化为电信号。使用互联网时的网络协议叫tcp/ip协议簇(一系列网络协议的集合)。
tcp/ip协议簇大体分为四层:1.应用层 2.传输层 3.网络层4.数据链路层。数据转电信号经过的过程复杂,分出的四个层把每一环节抽象成一个个功能点,把固定的功能点做成一个个固定的功能。相邻层间可互相调用,且上层不需要关心下层如何实现。
传输层控制数据传输方式,在tcp/ip协议里是tcp/ip传输方式,即面向连接的可靠连接方式。(面向连接:要使用tcp进行传输时,必须先和服务器连接上。 可靠连接:数据传输时,数据会封装成一个个小的数据封包,有数据传丢,tcp方式会进行数据重发。)
网络层会写清楚从哪发,发哪去。(IP地址包含来源和目的地)
数据链路层,底层的物理设备。
客户端将请求数据经过一系列变化传给服务器,服务器还会返回相应的数据,即再走回去。
程序员只需考虑应用层。
2.网络环境的构成:(网络连接)
每台计算机一个网口,多台计算机想连在一起,需要集线器(hub)协助,集线器里会提供很多网口。
也可用路由将多台计算机连在一起,网口没有集线器多。但集线器只是单纯的将电脑连在一起,很简单。路由相对复杂,维护了一个路由表,会设置一个网关,作为每台计算机使用的网段,不同网关设置的网段间不能互通。
两个路由器想互通起来,上层再加一个路由。上层路由可设置下层路由的网关是否可相互访问。路由里是有程序代码的。
若想访问互联网,则要先访问宽带的运营商,比如电信,联通,移动。都是中国的运营商,一般叫ISP。我们将自己的路由接到它们的大路由上。这样就能与更广的计算机互联。
这些电信运营商的网络运营是由NSP提供的,主干网络运营商(通常做跨国的)。
整个网络环境,由很多路由参与,通过维护路由表,找到要查找的地址。
3.分类
1.局域网(LAN,Local Area Network)
(仅仅是教室连起来,不和外面连,就建立了一个局域网。)
2.城域网(MAN,Metropolis Area Network)
(此概念已弱化,多个局域网连起来可能是校园,之后整个城市连起来。然后整个国家连起来,再整个世界连起来。城市的连接基本整到电信运营商身上了。)
3.广域网(WAN,Wide Area Network)
现在指互联网。 所有计算机连在一起的一个网络。
路由要配两个东西,LAN和WAN。LAN通常是自己的设备分配什么地址,WAN实际是和电信运营商连接的端口(也叫万口)。
无线网络主要研究电磁信号的发送方式。(电磁信号分两种,电磁辐射和电离辐射。对人体危害较大的是电离辐射。微波炉里就是电离辐射)
4.工作模式
1.专用服务器结构(Server-Based) b/S
又称为“工作站/文件服务器”结构,由若干台微机工作站与一台或多台文件服务器通过通信线路连接起来组成工作站存取服务器文件,共享存储设备。
2.客户机/服务器模式(client/server)
其中一台或几台较大的计算机集中进行共享数据库的管理和存取,称为服务器,而将其它的应用处理工作分散到网络中其它微机上去做,构成分布式的处理系统。
3.对等式网络:(Peer-to-Peer)
在对等式网络结构中,没有专用服务器 每一个工作站既可以起客户机作用也可以起服务器作用。
以下是不同数据的访问方式:
B/S: broswer/server 通过浏览器和服务器交互 (要和b/s区分开)
C/S: client/server pc端应用程序 通过应用程序客户端和服务器交互。
5.网络模型
上面的tcp/ip协议,是一套具体化的简化的网络模型。还有一套更抽象,更详细的网络模型,叫osi网络模型。
刚才的是对上面三层合并,最下面两层合并。
要发送数据,前三层对数据进行整理,传输层决定传输方式,并提供端口号,再通过网络层找到ip地址,最终由网线传出。
网络层涉及IP协议,和IP地址是两个概念。
2 常见网络协议
1.IP协议
规定了 不同的计算机 通过 ip地址 寻址
IP地址分成两个大的版本:
ipv4 255.255.255.255(最大取值 F.F.F.F) 32位 (平常使用的版本,最多32位)
大概40多亿个IP地址 ,实际并没有那么多,有一些特殊地址(占了十几亿):
剩下30多亿地址,大部分地址都留在了北美(北美定的)。
电信运营商并没有给每台计算机都分配IP端口,都是通过公共的IP地址在外进行访问,这些IP地址是电信运营商在维护,且给保护起来。自己的计算机地址没被外网发现是个好事,计算机想有外网地址需要申请。
对黑客来说,自己好像没什么价值,但可被中木马,成为网络环境的僵尸机,组成它的攻击网络,攻击别人,隐藏自己。故自己的计算机开放到外网不安全,此时又有一层机制,防火墙(一种硬件防火墙,从更底层过滤数据;一种软件防火墙,自己装。)。
只要不上非法网站,下一些东西,计算机是安全的。
1. 保留地址 192.168.xxx.xxx 和其他
(互联网里找不到这个IP开头的地址,给局域网用的。 其它是给企业或者自己留存用)
2. 回送地址 127.0.0.1 本机 需要过路由 (回送地址指本机,数据需要过路由)
localhost 本机 不过路由 (也可以代指本机,有路由也不过)
ipv6 128位 (与ipv4不太一样,可在控制台用ipconfig去查看)
更多 完全替换还有一定过程
2.端口 (不同应用程序发送接收数的出入口)
一些常见的不让用的端口:
3306 mysql默认端口
80 http协议默认端口
443 https协议默认端口
带不带s,在表示层有区别。 https在表示层多了一个tls加密。想要有这个功能需要买认证证书。对数据安全有要求(比如需要登录密码)的网站会使用https。
3.dns
(与网络传输关系不大,在运营商那有一套dns域名解析服务。)
域名解析服务 运营商维护
www.baidu.com 通过dns解析成对应的IP地址 xxx.xxx.xxx.xxx
hosts 文件 计算机本地映射
ip地址 域名
(若创建了一套软件,想发布到互联网上,让更多人去用。怎么做?从运营商要一个外网IP,再指定应用程序里的一个端口,发布出去。别人通过网络找到该计算机,再通过端口找到应用,并把数据发过来。问题则是开始不安全了,一般有外网设置,机房的设置很复杂。故一般网站挂到外网,都是找云服务)
3.应用层协议
http
https http+tls数据加密
常用格式如下:
https:// ip : 端口 /资源地址 ? key=value & key =value
(协议部分 ip部分 端口(使用默认端口可不填) /后的是资源地址 ?后使请求参数,参数格式是key=value & key =value )
其它的协议:
FTP(File Transfer Protocol,文件传输协议)
SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)
POP3/IMAP协议
P2P协议
....
(应用层协议可由程序员自己定义。)
4.数据传输协议
TCP 面向连接 可靠连接
连接三次握手
(客服端:我想传... 服务端:我准备好了,来吧 客户端:好的,那我给你传了 )
1.第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
2.第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
3.第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
关闭连接 四次挥手
(客户端:我要关了... 服务端:好的,我知道了 服务端:我准备好关了 客户端:我也准备好关了)
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
UDP 面向无连接 不可靠连接
常用在广播 网络直播 视频会议等
3常用类和方法
InetAddress ip相关内容 .getLocalHost() 获取本机 localHost.getHostName() 获取主机名 getAllByName(localHost.getHostName()) 根据主机名获取所有ip //URL 统一资源定位符 可以访问到的地址 //URI 统一资源标识符 地址的指代格式 URL 可以传入url地址 URL myurl = new URL("https://www.json.cn/"); System.out.println(myurl.getProtocol());//获取协议 System.out.println(myurl.getHost());//获取地址 System.out.println(myurl.getPort());//获取端口 System.out.println(myurl.getFile());//访问的资源地址和参数 System.out.println(myurl.getQuery());//访问的参数 openStream() 发送请求 并读取响应的数据 BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); 转成包装流 读取数据
socket 插座 (tcp方式)
ServerSocket 定义服务端 Socket 定义客户端 ServerSocket ss = new ServerSocket(7777); Socket accept = ss.accept(); socket.getInputStream() 输入流 读 socket.getOutStream() 输出流 写
DatagramSocket (udp方式)
DatagramSocket 如果用于发送,可使用无参 如果用于接收,需指定监听的端口(接收是本机接,数据是推过来的) DatagramPacket 用于封装数据包,两类,接收数据和发送数据。 发送数据需指定哪个地址发,接收数据不需要指定,从本机指定的对应端口接收即可。 udp是一种面向无连接的不可靠连接。
笔试,面试题:tcp和udp的异同。
(1)InetAddress类
public static void main(String[] args) throws Exception { //InetAddress类包含了一些和ip地址相关的信息 InetAddress localHost = InetAddress.getLocalHost(); //获取本机信息 System.out.println(localHost); //DESKTOP-NV851QM/192.168.101.13 主机名和本机的IP地址 InetAddress[] allByName = InetAddress.getAllByName(localHost.getHostName());//参数是主机名 for (InetAddress iad : allByName) { System.out.println(iad); //打印出ipv4和ipv6的信息 //DESKTOP-NV851QM/192.168.101.13 //DESKTOP-NV851QM/fe80:0:0:0:8945:ce46:4a09:91cf%23 } }
(2)URL类
public static void main(String[] args) throws Exception{ //URL类,在java.net包 URL是可以访问到的地址,也叫统一资源定位符 //URL(String) 参数是地址 URL myurl = new URL("https://www.baidu.com/"); //https://www.baidu.com/即一个URL,但不是直接访问百度用的。这个整体是用来解析传入的字符串 System.out.println(myurl.getHost()); //www.baidu.com(超链接) 获取地址 System.out.println(myurl.getPort()); //-1 获取端口 System.out.println(myurl.getFile()); /// 获取访问的资源地址和参数 System.out.println(myurl.getQuery()); //null 访问的参数 System.out.println(myurl.getProtocol()); //https 获取协议 URL myurl1 = new URL("https://123.123.123.123:8888/test?param=hello"); //参数可自己写 System.out.println(myurl1.getHost()); //123.123.123.123 System.out.println(myurl1.getPort()); //8888 System.out.println(myurl1.getFile()); // /test?param=hello System.out.println(myurl1.getQuery()); //param=hello System.out.println(myurl1.getProtocol()); //https //创建URL对象。可打开链接 InputStream inputStream = myurl.openStream(); //最基本的字节流(读取,需要建缓冲区,从缓冲区拿) 读服务器传回来的内容 //用字符流更方便,故字节流转字符流 BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); // String s = br.readLine(); // System.out.println(s); //<!DOCTYPE html> 读到了html代码,因为换行,只读到了第一行 //若全部读出,则循环 String s; while ((s = br.readLine())!=null){ System.out.println(s); } //URL底层还是走的tcp/ip协议,将数据发出去,将内容拿回来 //最简单的爬虫就是获取不想手动操作的资源,但一般使用python写爬虫(因为有很多现成的工具,比如Xpath,一种类似正则表达式 //的匹配格式,根据网页里标签的格式和特征抓出特定的内容)。 //爬虫就是抓取特征数据+网络io 本地io //网络的本质就是数据的传输,或者说不同应用之间传输数据 //URI叫统一资源标识符,通常是地址的指代格式,标识网络的地址,但可能访问不通 }
(3)socket类,本身是插座的意思,在api也叫套接字,指两台计算机通讯的端点。
两台计算机交互,要有连通,就像插座一样。
ServerSocket 定义服务端(定义好后被动等待客户端请求)
(作为服务端讲,服务器就是本机,代码在哪运行,该计算机就是服务器。)
Socket 定义客户端 (创建发往服务端的请求)
过程:
1.ServerSocket创建服务端(指定端口),用accept()方法开启监听端口
2.Socket创建客户端(指定ip和端口),将请求发送到指定服务器
3.若此时服务器开启了监听端口,则请求数据过来,双方就互相创建了和对方连通的socket对象,就能互相发数据
4.客户端给服务端发数据,即往外发,用输出流;服务器使用输入流读取客户端数据。
同样,服务器给客户端发数据,用输出流,对应客户端接收数据用输入流。
这样一来一回,就完成了一次基本的数据交互。
注意点:1.使用socket进行数据传输,必然是两个应用程序;
2.socket是基于tcp/ip协议相关的封装的一个工具类,故使用ip+端口的方式查找,且发送数据的方式是tcp,故使用socket是一种面向链接的可靠地链接。
相关代码如下:
public class MyServeSocket { public static void main(String[] args) throws Exception{ //参数可传端口号port。 //0-1023是系统服务的端口,不能用;1024-65535这些端口,只要别人不用,都可用(5000以下的端口被有名应用占用,比如 //mysql是3306) ServerSocket ss = new ServerSocket(7777); System.out.println("开始监听7777端口,等待访问"); Socket accept = ss.accept(); //开启服务监听,返回值是socket对象(谁连我,就代表和谁连接的socket对象) //若有请求连入 InputStream inputStream = accept.getInputStream(); //读客户端发送的数据(拿到输入流) DataInputStream dis = new DataInputStream(inputStream); System.out.println(dis.readUTF()); //读请求发送来的数据,并打印到控制台 dis.close(); //流用完一定要关掉 accept.close(); //socket不用也关掉,调用关闭会自动调用四次挥手,即连接的三次握手和关闭的四次挥手已封装在其中 //注:程序运行期间,异常终止,对方会报错,服务端数据还没传过去。 } }
public class MyClientSocket { public static void main(String[] args) throws Exception{ //Socket(String host,int port) 可指定地址和端口。 Socket socket = new Socket("127.0.0.1",7777); //服务端是本机可传127.0.0.1 //通过输出流发数据 OutputStream outputStream = socket.getOutputStream(); DataOutputStream dos = new DataOutputStream(outputStream); dos.writeUTF("hello service"); //关闭流时,若为输出流,先刷新。虽然四次挥手会推流,但还是在关闭流前主动做一次推流 dos.flush(); dos.close(); socket.close(); } }
先运行服务端,再开启客户端。有两个main程序,故为两个应用。以上只做了一半(客户端给服务端发送请求),还要做服务端给客户端返回数据的操作。
public class MyServeSocket { public static void main(String[] args) throws Exception{ ServerSocket ss = new ServerSocket(7777); System.out.println("开始监听7777端口,等待访问"); Socket accept = ss.accept(); InputStream inputStream = accept.getInputStream(); DataInputStream dis = new DataInputStream(inputStream); System.out.println(dis.readUTF()); //服务端往回写的过程: OutputStream outputStream = accept.getOutputStream(); //获取输出流 DataOutputStream dos = new DataOutputStream(outputStream); dos.writeUTF("hello client"); dis.close(); //同样需要关闭 dos.flush(); dos.close(); accept.close(); } }
public class MyClientSocket { public static void main(String[] args) throws Exception{ Socket socket = new Socket("127.0.0.1",7777); OutputStream outputStream = socket.getOutputStream(); DataOutputStream dos = new DataOutputStream(outputStream); dos.writeUTF("hello service"); //客户端读操作 InputStream inputStream = socket.getInputStream(); DataInputStream dis = new DataInputStream(inputStream); System.out.println(dis.readUTF()); dos.flush(); dos.close(); //用完关闭 dis.close(); socket.close(); } }
要让服务端,客户端多次执行,只要加循环:
服务端,客户端要写入的数据也不写死,从控制台读:
相当于建立聊天室:
public class MyServeSocket { public static void main(String[] args) throws Exception{ ServerSocket ss = new ServerSocket(7777); System.out.println("开始监听7777端口,等待访问"); Scanner sc = new Scanner(System.in); while (true){ Socket accept = ss.accept(); InputStream inputStream = accept.getInputStream(); DataInputStream dis = new DataInputStream(inputStream); System.out.println(dis.readUTF()); OutputStream outputStream = accept.getOutputStream(); DataOutputStream dos = new DataOutputStream(outputStream); dos.writeUTF(sc.nextLine()); dis.close(); dos.flush(); dos.close(); accept.close(); } } }
public class MyClientSocket { public static void main(String[] args) throws Exception{ while (true){ Socket socket = new Socket("127.0.0.1",7777); Scanner sc = new Scanner(System.in); OutputStream outputStream = socket.getOutputStream(); DataOutputStream dos = new DataOutputStream(outputStream); dos.writeUTF(sc.nextLine()); InputStream inputStream = socket.getInputStream(); DataInputStream dis = new DataInputStream(inputStream); System.out.println(dis.readUTF()); dos.flush(); dos.close(); dis.close(); socket.close(); } } }
问题:一方发多条信息会卡。现在每一方都只有一个主线程,会阻塞主线程的有两个:一个是accept监听,一个是控制台输入。并且,循环流程是一读一写,对方是一写一度,同时写三个过来,程序会卡在输入(sc.nextLine())。
解决方案:找个人专门去写,再找个人专门去读。此人即子线程。
public class ReadWriteThread { Scanner sc = new Scanner(System.in); //socket对象不能随便创建,一般用构造传参(不同类间使用对象,一般两种方式:方法传对象或者构造传对象) Socket soc; public ReadWriteThread(Socket soc){ this.soc = soc; } public void myRead() throws Exception{ //定义读的方法 InputStream inputStream = soc.getInputStream(); DataInputStream dis = new DataInputStream(inputStream); System.out.println(dis.readUTF()); } public void myWrite() throws Exception{ OutputStream outputStream = soc.getOutputStream(); DataOutputStream dos = new DataOutputStream(outputStream); dos.writeUTF(sc.nextLine()); } }
public class MyServeSocket { public static void main(String[] args) throws Exception{ ServerSocket ss = new ServerSocket(7777); System.out.println("开始监听7777端口,等待访问"); Scanner sc = new Scanner(System.in); Socket accept = ss.accept(); ReadWriteThread rwt = new ReadWriteThread(accept); new Thread(()->{ try { while (true){ rwt.myRead(); } } catch (Exception e) { e.printStackTrace(); } }).start(); new Thread(()->{ try { while (true){ rwt.myWrite(); } } catch (Exception e) { e.printStackTrace(); } }).start(); } }
public class MyClientSocket { public static void main(String[] args) throws Exception{ Socket socket = new Socket("127.0.0.1",7777); ReadWriteThread rwt = new ReadWriteThread(socket); new Thread(()->{ while (true){ try { rwt.myRead(); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(()->{ while (true){ try { rwt.myWrite(); } catch (Exception e) { e.printStackTrace(); } } }).start(); } }
(4)udp方式(面向无连接)
与tcp方式(面向连接的方式)的区别:
udp方式不可靠,传输时丢了就丢了。适合用在多个地址的发送,谁在监听,谁获取信息。
发送代码如下:
public class UDPSend { public static void main(String[] args) throws Exception{ DatagramSocket datagramSocket = new DatagramSocket(); //数据包分为用于发送和接收的数据包 /*Constructor and Description DatagramPacket(byte[] buf, int length) 构造一个 DatagramPacket用于接收长度的数据包 length 。 DatagramPacket(byte[] buf, int offset, int length) 构造一个 DatagramPacket用于接收长度的分组 length ,指定偏移到缓冲器中。 DatagramPacket(byte[] buf, int length, InetAddress address, int port) 构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。 DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) 构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。 DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。 DatagramPacket(byte[] buf, int length, SocketAddress address) 构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。*/ //构建DatagramSocket对象 DatagramPacket(byte[] buf, int length, InetAddress address, int port) //数组,数据包长度,InetAddress对象,指定发送的端口 String mystr = "hello jack"; InetAddress localHost = InetAddress.getLocalHost(); DatagramPacket dp = new DatagramPacket(mystr.getBytes(),mystr.length(),localHost,7890); datagramSocket.send(dp); //send(DatagramPacket) 参数是数据包 发送数据 System.out.println("数据包发送出去了"); } }
接收代码如下:
public class UDPRecive { public static void main(String[] args) throws Exception{ //接收数据要绑定一个监听的接收数据的端口 DatagramSocket datagramSocket = new DatagramSocket(7890); //DatagramPacket(byte[] buf, int length) byte[] mydata = new byte[1024]; DatagramPacket dp = new DatagramPacket(mydata,mydata.length); datagramSocket.receive(dp); //用数据包接收数据 System.out.println(new String(mydata)); } }