网络编程
网络协议
-
如同人与人之间相互交流是需要遵循一定的规则(如语言)一样,计算机之间能够进行相互通信是因为它们都共同遵守一定的规则,即网络协议。OSI参考模型和TCP/IP模型在不同的层次中有许多不同的网络协议,如图所示:
-
网络协议之间的关系图如下:
-
IP协议(Internet protocol)
IP协议的作用在于把各种数据包准备无误的传递给对方,其中两个重要的条件是IP地址和MAC地址。由于IP地址是稀有资源,不可能每个人都拥有一个IP地址,所以我们通常的IP地址是路由器给我们生成的IP地址,路由器里面会记录我们的MAC地址。而MAC地址是全球唯一的。举例,IP地址就如同是我们居住小区的地址,而MAC地址就是我们住的那栋楼那个房间那个人。IP地址采用的IPv4格式,目前正在向IPv6过渡。
-
TCP协议(Transmission Control Protocol)
TCP(传输控制协议)是面向连接的传输层协议。TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。TCP协议采用字节流传输数据。
-
三次握手与四次挥手
TCP是面向连接的协议,因此每个TCP连接都有3个阶段:连接建立、数据传送和连接释放。连接建立经历三个步骤,通常称为“三次握手”。 -
TCP三次握手过程如下:
- 第一次握手(客户端发送请求)
客户机发送连接请求报文段到服务器,并进入SYN_SENT状态,等待服务器确认。发送连接请求报文段内容:SYN=1,seq=x;SYN=1意思是一个TCP的SYN标志位置为1的包,指明客户端打算连接的服务器的端口;seq=x表示客户端初始序号x,保存在包头的序列号(Sequence Number)字段里。 - 第二次握手(服务端回传确认)
服务器收到客户端连接请求报文,如果同意建立连接,向客户机发回确认报文段(ACK)应答,并为该TCP连接分配TCP缓存和变量。服务器发回确认报文段内容:SYN=1,ACK=1,seq=y,ack=x+1;SYN标志位和ACK标志位均为1,同时将确认序号(Acknowledgement Number)设置为客户的ISN加1,即x+1;seq=y为服务端初始序号y。 - 第三次握手(客户端回传确认)
客户机收到服务器的确认报文段后,向服务器给出确认报文段(ACK),并且也要给该连接分配缓存和变量。此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。客户端发回确认报文段内容:ACK=1,seq=x+1,ack=y+1;ACK=1为确认报文段;seq=x+1为客户端序号加1;ack=y+1,为服务器发来的ACK的初始序号字段+1。
- TCP四次挥手过程如下:
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
- TCP客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态。发送报文段内容:FIN=1,seq=u;FIN=1表示请求切断连接;seq=u为客户端请求初始序号。
- 服务端收到这个FIN,它发回一个ACK给客户端,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号;服务端进入CLOSE_WAIT状态。发送报文段内容:ACK=1,seq=v,ack=u+1;ACK=1为确认报文;seq=v为服务器确认初始序号;ack=u+1为客户端初始序号加1。
- 服务器关闭客户端的连接后,发送一个FIN给客户端,服务端进入LAST_ACK状态。发送报文段内容:FIN=1,ACK=1,seq=w,ack=u+1;FIN=1为请求切断连接,ACK=1为确认报文,seq=w为服务端请求切断初始序号。
- 客户端收到FIN后,客户端进入TIME_WAIT状态,接着发回一个ACK报文给服务端确认,并将确认序号设置为收到序号加1,服务端进入CLOSED状态,完成四次挥手。发送报文内容:ACK=1,seq=u+1,ack=w+1;ACK=1为确认报文,seq=u+1为客户端初始序号加1,ack=w+1为服务器初始序号加1。
- 注意:为什么连接的时候是三次握手,关闭的时候却是四次挥手?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭socket,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文,我收到了”。只有等到服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四步挥手。
UDP协议(User Datagram Protocol)
- UDP,用户数据报协议,它是TCP/IP协议簇中无连接的运输层协议。
- UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
- 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务器可同时向多个客户端传输相同的消息。
- UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
- 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
- UDP使用尽量最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态表。
- UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部受就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。
UDP协议格式
UDP协议由两部分组成:首部和数据。其中,首部仅有8个字节,包括源端口和目的端口、长度(UDP用于数据报的长度)、校验。
TCP与UDP的区别
- TCP基于连接,UDP是无连接的;
- 对系统资源的要求,TCP较多,UDP较少;
- UDP程序结构较简单;
- TCP是流模式,而UDP是数据报模式;
- TCP保证数据正确性,而UDP可能丢包;TCP保证数据顺序,而UDP不保证;
Socket概述
-
Java的网络编程主要涉及到的内容是Socket编程。Socket套接字,就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。
-
应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。-
-
Socket,实际上是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。实际上,Socket跟TCP/IP协议没有必然的关系,Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现,只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、accept、send、read和write等等。网络有一段关于socket和TCP/IP协议关系的说法比较容易理解:
-
“TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”
-
实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。socket是对端口通信开发的工具,它要更底层一些。
Socket整体流程
- Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。
- 客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
示例
TCP协议通信
- 服务器端 Server
public class Server {
public static void main(String[] args) throws Exception {
int port = 8080;
int clientNo = 1;
ServerSocket server = new ServerSocket(port);
ExecutorService threadPool = Executors.newCachedThreadPool();
while (true) {
Socket socket = server.accept();
threadPool.execute(new SingleServer(socket,clientNo));
clientNo++;
}
}
}
class SingleServer implements Runnable {
public Socket socket;
private int clientNo;
public SingleServer(Socket socket, int clientNo) {
this.socket = socket;
this.clientNo = clientNo;
}
public SingleServer(int port) {
}
@Override
public void run() {
try {
DataInputStream dis = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
do {
double in = dis.readDouble();
System.out.println("输入的边长为:"+in);
dos.writeDouble(in * in);
dos.flush();
} while (dis.readInt() != 0);
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("客户端"+clientNo+"通信结束");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 客户端 Client
public class Client {
public static void main(String[] args) throws Exception{
Socket socket = new Socket("127.0.0.1", 8080);
DataInputStream dis = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("输入边长");
double length = sc.nextDouble();
dos.writeDouble(length);
dos.flush();
double area = dis.readDouble();
System.out.println("面积:"+area);
socket.close();
}
}
}
UDP协议通信
- 发送方
public class UDPSend {
public static void main(String[] args) throws Exception {
String str = "abc";
DatagramSocket datagramSocket = new DatagramSocket();
DatagramPacket datagramPacket = new DatagramPacket(str.getBytes("utf8"), str.length(),
InetAddress.getByName("localhost"), 9999);
System.out.print("发送了:");
datagramSocket.send(datagramPacket);
System.out.println(datagramPacket);
datagramSocket.close();
}
}
- 接收方
public class UDPRecv {
public static void main(String[] args) throws Exception{
DatagramSocket datagramSocket = new DatagramSocket(9999);
DatagramPacket datagramPacket = new DatagramPacket(new byte[1024], 1024);
datagramSocket.receive(datagramPacket);
datagramSocket.close();
byte[] data = datagramPacket.getData();
String str = new String(data, 0, datagramPacket.getLength(),"utf8");
System.out.println(str);
}
}
聊天室案例
- 服务器端
public class Server {
public static void main(String[] args) {
System.out.println("============服务开启============");
System.out.println("等待用户接入");
PrintWriter pw = null;
Scanner input = null;
Scanner inScanner = null;
ServerSocket ss = null;
try {
// 创建Socket
ss = new ServerSocket(9998);
// 创建一个接收连接客户端的对象
Socket accept = ss.accept();
System.out.println(accept.getInetAddress()+"连接成功!");
pw = new PrintWriter(accept.getOutputStream());
pw.println("您已连接到服务器!");
pw.flush();
// 开启键盘输入
input = new Scanner(System.in);
inScanner = new Scanner(accept.getInputStream());
while (inScanner.hasNextLine()) {
String indata = inScanner.nextLine();
System.out.println("客户端:"+indata);
System.out.println("我(客户端):");
String keyboarddata = input.nextLine();
System.out.println("服务器:"+ keyboarddata);
System.out.println("我(服务器):");
pw.println(keyboarddata);
pw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 客户端
public class Client {
public static void main(String[] args) {
System.out.println("============连接开启============");
Socket socket = null;
Scanner keyboardScanner = null;
Scanner inScanner = null;
PrintWriter pw = null;
try {
socket = new Socket("localhost", 9998);
// 读入
inScanner = new Scanner(socket.getInputStream());
System.out.println(inScanner.nextLine());
pw = new PrintWriter(socket.getOutputStream());
System.out.println("我(客户端):");
// 先读取键盘录入方可向服务器端发送消息
keyboardScanner = new Scanner(System.in);
//录入键盘
while (keyboardScanner.hasNextLine()) {
String keyboarddata = keyboardScanner.nextLine();
// 展示到我方控制台
System.out.println("我(客户端):"+keyboarddata);
pw.println(keyboarddata);
pw.flush();
// 阻塞等待接收服务器端的消息
String indata = inScanner.nextLine();
System.out.println("服务器端:"+indata);
System.out.println("我(客户端):");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}