To u&me: 努力经营当下,直至未来明朗
一个人最大的痛苦来源于对自己无能的愤怒!
Hi 这里是已经快要秃头的宝贝儿
本文主要内容:套接字的使用以及 TCP、UDP介绍,还有客户端&服务器的简单示例代码。
(此处只列出相关名称,不作具体解释)
有连接
可靠传输
面向字节流
有接收缓冲区,也有发送缓冲区
大小不限
③ 对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
2)数据报套接字:使用传输层UDP协议
① UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
② 以下为UDP的特点):
无连接
不可靠传输
面向数据报
有接收缓冲区,无发送缓冲区
大小受限:一次最多传输64k
③ 对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节;而不能分100次,每次接收1个字节。
3)原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。(简单了解即可。)
2)UDP:
无连接、不可靠传输、面向数据报、全双工
① 有无连接:如打电话是有连接的,发微信是没连接的。即有连接是要先建立连接后才可以通信,无连接是可以直接进行发送。
② 可靠传输:不是说A给B发的数据100%能够让B收到,而是A可以知道B有没有收到。
③ 字节流:IO的章节有介绍,如InputStream、OutputStream,即TCP是基于流的。
④ 面向数据报:UDP是以“数据报”为基本单位的
⑤ 全双工相对的词是半双工。
全双工:一个通道,双向通信; 半双工:一个通道,单向通信。
网络通信一般都是全双工。
1)客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景一般都是不同主机。
2) 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
3) Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议 也需要考虑。
4) 关于端口被占用的问题:
如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。对于java进程来说,端口被占用的常见报错信息如下:
此时需要检查进程B绑定的是哪个端口,再查看该端口被哪个进程占用。以下为通过端口号查进程的方式:
在cmd输入 netstat -ano | findstr 端口号 ,则可以显示对应进程的pid ,或者使用jdk中的bin/jconsole查看阻塞位置。
2)DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报
① DatagramPacket 构造方法:
② DatagramPacket 方法:
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用InetSocketAddress 来创建。
3)InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
Socket本质上是文件。
【狭义的文件就是存储在磁盘上的文件; 广义的文件:操作系统把各种硬件设备和软件资源都抽象成文件,统一按照文件的方式来管理。】
(socket对应到网卡这个硬件设备,操作系统也是把网卡当做文件来管理。通过网卡发送数据就是“写文件”,通过网卡接收数据就是“读文件”)
DatagramSocket是网卡的代言人,可以操作网卡;而DatagramPacket就代表一个UDP数据报,也就是一次发送/接收的基本单位。
回显服务器echo server (客户端发啥,服务器就返回啥 :其实没有任何实际意义)
一个正经的服务器其实有一个非常关键的环节:根据请求计算出响应。
服务器启动会绑定端口号,目的就是为了能够让客户端明确是访问主机上的哪个进程!
要想通过端口确定一个进程,就需要在进程启动的时候绑定一个端口,并且在通常情况下一个端口只能被一个进程绑定。
如:数据库服务器启动就会绑定3306端口
【由于服务器不确定客户端啥时候发请求过来,所以要时刻“严阵以待”,通常情况就使用 while true来作为一个循环】
DatagramSocket socket 调用的receive()方法,参数是DatagramPacket类型的输出型参数,没有返回值。 也就是说:调用receive的时候就需要构造一个空的DatagramPacket对象,然后把对象交给receive,在receive里面负责把从网卡读到的数据给填充到这个对象中。
服务器收到的包裹 DatagramPacket requestPacket上就包含了客户端的地址信息(客户端ip和端口号),后面服务器要返回响应的时候直接从这里取就行
requestPacke 调用getSocketAddress() 方法,返回的是InetSocketAddress类型的数据,可以同时包含客户端的IP和端口信息。
【注:服务器端的构造方法传入的参数是服务器自己绑定的端口号;服务器端的ip一般不用写,因为这里的ip就是本机的ip,程序在哪个主机上启动,对应的ip就是主机自身的ip。(如果一个主机有多个网卡,涉及到了多个IP,并且如果你只希望你的服务器被通过某个指定的IP能够访问,此时就需要手动指定IP; 一般来说,如果不进行手动指定IP,此时就会针对所有的主机IP都生效。)】
客户端的构造方法传入的参数是服务器端的IP地址和服务器端口号(也就是服务器端绑定的端口号)
(一个电脑一个IP)
客户端new DataGramSocket时是没有指定参数的,服务器端是传入参数的,因为服务器要绑定端口号。
客户端给服务器发送一个数据的时候:
①客户端自己的主机IP:源IP
②客户端是没有手动绑定一个端口号的,操作系统会自动分配一个空闲的端口:源端口
③服务器的主机:目的IP
④服务器绑定的端口:目的端口)
(客户端手动指定端口也不是不行,但是不确定该端口是否是空闲的。)
为啥服务器不害怕端口冲突,而客户端就担心端口冲突呢?
因为服务器是在程序员手里的,运行的程序分别是啥端口是可控的; 而客户端是在用户自己的电脑中,用户电脑上安装的软件等就可能占用了指定的端口号。
客户端的ip就是运行客户端程序的主机IP,不需要再代码中添加;只需要告诉客户端我们所访问的服务器IP就行啦。
补充:端口号是一个16位(二进制)的整数,即0~ 65535,但是我们使用的一般是从1024开始的端口号,这些端口号可以任意使用;但是0~1023成为“知名端口号”,被一些比较知名的应用程序占用。
(补充:DataGramSocket中的receive在阻塞时是死等)
注意:服务器端与客户端的工作流程!
【服务器:①读取请求并解析; ②根据请求计算响应; ③将请求写回客户端; ④打印日志
客户端:①从控制台读取用户输入的内容; ②构造一个UDP请求,发送给服务器; ③从服务器读取UDP响应数据,并解析; ④把服务器的响应显示到控制台】
可以把两个程序放到不同主机上也是可以通信的,但是要确保服务器的地址是可以访问到的。 注:NAT机制下,我的电脑IP只是一个局域网内部使用的IP,不能在广域网中直接使用。
服务器是要给多个客户端提供服务的。那么如何在IDEA中启动多个客户端?
需要自己修改一下配置,具体如下所示:
(一定要选中“客户端”,然后选择完成后点击“ok”)
要求:英译汉
(核心其实和回显服务器差不多,差别只是在process根据请求计算响应的逻辑,那就直接继承回显服务器,然后再重写process方法就行。)
其实一个服务器的基本流程都是差不多的,最核心的区别其实就是process“根据请求计算响应”,因为不同的服务器有不同的业务逻辑
(补充:Map中的getOrDefault(默认值)方法,找到key就输出对应的value,没有找到就输出默认值)
② ServerSocket 方法:
2)Socket API :既会给服务器用,又会给客户端用
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,即用来与对方收发数据的。
① Socket 构造方法:
② Socket 方法:
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。
实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
(TCP是有、可、字节流)
如果客户端没有没有建立连接,此时服务器就会阻塞到accept;
而如果有一个客户端过来了,此时就会进入processConnection方法中,如果此时客户端没有发消息则代码会阻塞在hasNext处,所以此时就无法第二次调用accept,也就没办法处理第二个客户端了
==》此时其实就是希望:既能够快速重复的调用accept,又能够循环的处理客户端的请求
==》所以就使用多线程处理processConnection:每个客户端进来都分配一个新的线程负责处理
==》但是依旧有频繁创建、释放线程的开销,所以改为使用线程池(注意创建方式!)
(IO流对象本身是线程安全的)
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
- 客户端发送请求和服务端解析请求要使用相同的数据格式。
- 服务端返回响应和客户端解析响应也要使用相同的数据格式。
- 请求格式和响应格式可以相同,也可以不同。
- 约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。
- 可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。
- 如果是使用知名协议,这个动作也称为封装
- 如果是使用小众协议(包括自定义协议),这个动作也称为序列化,一般是将程序中的对象转换为特定的数据格式。
2)接收端应用程序,接收数据时的数据转换,即对接收数据时的数据解析动作来说:
- 如果是使用知名协议,这个动作也称为分用
- 如果是使用小众协议(包括自定义协议),这个动作也称为反序列化,一般是基于接收数据特定的格式,转换为程序中的对象
① 对于定长的字段: 可以基于长度约定,如int字段,约定好4个字节即可
② 对于不定长的字段:可以约定字段之间的间隔符,或最后一个字段的结束符,如换行符间隔,\3符号结束等等
③ 除了该字段“数据”本身,再加一个长度字段,用来标识该“数据”长度;即总共使用两个字段:
“数据”字段本身,不定长,需要通过“长度”字段来解析;
“长度”字段,标识该“数据”的长度,即用于辅助解析“数据”字段。
【回顾】