要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识
端口网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了。也就是应用程序的标识
协议通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。常见的协议有UDP协议和TCP协议
IP地址:是网络中设备的唯一标识
IP地址分为两大类IPv4:是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是“11000000 1010100000000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP地址经常被写成十进制的形式,中间使用符号“.”分隔不同的字节。于是,上面的IP地址可以表示为“192.168.1.66”。IP地址的这种表示法叫做“点分十进制表示法”,这显然比1和0容易记忆得多
IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8 组十六进制数,这样就解决了网络地址资源数量不够的问题
InetAddress:此类表示Internet协议(IP)地址 相关方法
方法名
说明
static InetAddress getByName(String host)
确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址
String getHostName()
获取此IP地址的主机名
String getHostAddress()
返回文本显示中的IP地址字符串
/* InetAddress 此类表示Internet协议(IP)地址 public static InetAddress getByName(String host):确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址 public String getHostName():获取此IP地址的主机名 public String getHostAddress():返回文本显示中的IP地址字符串 */ public class Test { public static void main(String[] args) throws UnknownHostException { //public static InetAddress getByName(String host):确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址 //InetAddress hostAddress = InetAddress.getByName("Toroidals"); InetAddress hostAddress = InetAddress.getByName("192.168.109.1"); //public String getHostName():获取此IP地址的主机名 System.out.println(hostAddress.getHostName()); //public String getHostAddress():返回文本显示中的IP地址字符串 System.out.println(hostAddress.getHostAddress()); } } /* Toroidals 192.168.109.1 */设备上应用程序的唯一标识端口,用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
计算机网络中,连接和通信的规则被称为网络通信协议
UDP协议
用户数据报协议(User Datagram Protocol)UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。 由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议TCP协议
传输控制协议 (Transmission Control Protocol)TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”标志位(Flags):共6个,即URG、ACK、PSH、RST、SYN、FIN等。具体含义如下:
URG:紧急指针(urgent pointer)有效。 ACK:确认序号有效。 PSH:接收方应该尽快将这个报文交给应用层。 RST:重置连接。 SYN:发起一个新连接。 FIN:释放一个连接。
三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠 第一次握手,客户端向服务器端发出连接请求,等待服务器确认第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求第三次握手,客户端再次向服务器端发送确认信息,确认连接完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等
四次挥手
1)首先客户端想要释放连接,向服务器端发送一段TCP报文,其中:
标记位为FIN,表示“请求释放连接“;序号为Seq=U;随后客户端进入FIN-WAIT-1阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。
(2)服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT阶段(半关闭状态)并返回一段TCP报文,其中:
标记位为ACK,表示“接收到客户端发送的释放连接的请求”;序号为Seq=V;确认号为Ack=U+1,表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值;随后服务器端开始准备释放服务器端到客户端方向上的连接。客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段
前"两次挥手"既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了
(3)服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文,其中:
标记位为FIN,ACK,表示“已经准备好释放连接了”。注意:这里的ACK并不是确认收到服务器端报文的确认报文。序号为Seq=W;确认号为Ack=U+1;表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值。随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据。
(4)客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文,其中:
标记位为ACK,表示“接收到服务器准备好释放连接的信号”。序号为Seq=U+1;表示是在收到了服务器端报文的基础上,将其确认号Ack值作为本段报文序号的值。确认号为Ack=W+1;表示是在收到了服务器端报文的基础上,将其序号Seq值作为本段报文确认号的值。随后客户端开始在TIME-WAIT阶段等待2MSL
UDP协议是一种不可靠的网络协议,它在通信的两端各建立一个Socket对象,但是这两个Socket只是发
送,接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念
Java提供了DatagramSocket类作为基于UDP协议的Socket 构造方法
方法名
说明
DatagramSocket()
创建数据报套接字并将其绑定到本机地址上的任何可用端口
DatagramPacket(byte[] buf,int len,InetAddress add,int port)
创建数据包,发送长度为len的数据包到指定主机的指定端口
相关方法
方法名
说明
void send(DatagramPacket p)
发送数据报包
void close()
关闭数据报套接字
void receive(DatagramPacket p)
从此套接字接受数据报包
UDP发送数据
/* UDP发送数据的步骤 1:创建发送端的Socket对象(DatagramSocket) 2:创建数据,并把数据打包 3:调用DatagramSocket对象的方法发送数据 4:关闭发送端 */ public class SendData { public static void main(String[] args) throws IOException { //创建发送端的Socket对象(DatagramSocket) // DatagramSocket() 构造数据报套接字并将其绑定到本地主机上的任何可用端口 DatagramSocket datagramSocket = new DatagramSocket(); //创建数据,并把数据打包 //DatagramPacket(byte[] buf, int length, InetAddress address, int port) //构造一个数据包,发送长度为 length的数据包到指定主机上的指定端口号。 byte[] bytes = "hello,UDP。它来了".getBytes(); // int len = bytes.length; // InetAddress address = InetAddress.getByName("192.168.109.1"); // int port = 10010; // DatagramPacket datagramPacket = new DatagramPacket(bytes, len, address, port); DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.109.1"), 10010); //调用DatagramSocket对象的方法发送数据 //void send(DatagramPacket p) 从此套接字发送数据报包 datagramSocket.send(datagramPacket); //关闭发送端 //void close() 关闭此数据报套接字 datagramSocket.close(); } }UDP接受数据
/* UDP接收数据的步骤 1:创建接收端的Socket对象(DatagramSocket) 2:创建一个数据包,用于接收数据 3:调用DatagramSocket对象的方法接收数据 4:解析数据包,并把数据在控制台显示 5:关闭接收端 */ public class ReceiveData { public static void main(String[] args) throws IOException { //创建接收端的Socket对象(DatagramSocket) //DatagramSocket(int port) 构造数据报套接字并将其绑定到本地主机上的指定端口 DatagramSocket datagramSocket = new DatagramSocket(10010); //创建一个数据包,用于接收数据 //DatagramPacket(byte[] buf, int length) 构造一个 DatagramPacket用于接收长度为 length数据包 byte[] bytes = new byte[1024]; int len = bytes.length; DatagramPacket dataPacket = new DatagramPacket(bytes, len); //调用DatagramSocket对象的方法接收数据 datagramSocket.receive(dataPacket); //解析数据包,并把数据在控制台显示 //byte[] getData()s 返回数据缓冲区 byte[] datas = dataPacket.getData(); //int getLength() 返回要发送的数据的长度或接收到的数据的长度 int length = dataPacket.getLength(); System.out.println("传输内容是: " + new String(bytes, 0, length)); //关闭接收端 datagramSocket.close(); } }多开存窗口
点击第一个就会出来下面的菜单了,我们选择Edit Configurations进入选项编辑
右上角的Allow parallel run,我们把勾给打上。
public static void main(String[] args) throws IOException { //创建发送端的Socket对象(DatagramSocket) DatagramSocket datagramSocket = new DatagramSocket(); //获取键盘录入信息 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); String line; while((line = bufferedReader.readLine()) != null){ //输入内容是886就结束 if (line.equals("886")){ break; } //把数据打包 byte[] bytes = line.getBytes(); int port = 10011; InetAddress address = InetAddress.getByName("192.168.109.1"); DatagramPacket dataPacket = new DatagramPacket(bytes, bytes.length, address, port); //调用DatagramSocket对象的方法发送数据 datagramSocket.send(dataPacket); } //关闭发送端 datagramSocket.close(); } public static void main(String[] args) throws IOException { //创建接收端的Socket对象(DatagramSocket) DatagramSocket dataReceive = new DatagramSocket(10011); while(true){ //创建一个数据包,用于接收数据 byte[] bytes = new byte[1024]; DatagramPacket dataPacket = new DatagramPacket(bytes, bytes.length); //调用DatagramSocket对象的方法接收数据 dataReceive.receive(dataPacket); //解析数据包,并把数据在控制台显示 int len = dataPacket.getLength(); byte[] datas = dataPacket.getData(); String input = new String(datas, 0, len); System.out.println("'" + input + "'"); if (input.equals("shutdown")){ break; } } //关闭接收端 dataReceive.close(); } /* '我是第一个send' '我是第二个send' '我是第三个' '告辞,886' '在下也告辞了' '我要结束你' 'shutdown' */TCP发送数据
Java中的TCP通信
Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
Java为客户端提供了Socket类,为服务器端提供了ServerSocket类
构造方法
方法名
说明
Socket(InetAddress address,int port)
创建流套接字并将其连接到指定IP指定端口号
Socket(String host, int port)
创建流套接字并将其连接到指定主机上的指定端口号
相关方法
方法名
说明
InputStream getInputStream()
返回此套接字的输入流
OutputStream getOutputStream()
返回此套接字的输出流
/* TCP发送数据的步骤 1:创建客户端的Socket对象(Socket) 2:获取输出流,写数据 3:释放资源 */ public class sendByTcp { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号 Socket socket = new Socket(InetAddress.getByName("192.168.119.1"), 10010); //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 //Socket socket = new Socket("192.168.119.1", 10010); //获取输出流,写数据 //OutputStream getOutputStream() 返回此套接字的输出流 OutputStream outputStream = socket.getOutputStream(); outputStream.write("Tcp,我来了".getBytes()); //释放资源 //outputStream.close(); //outputStream由socket对象创建,socket对象一释放它也就释放了 socket.close(); } }TCP接收数据
方法名
说明
ServletSocket(int port)
创建绑定到指定端口的服务器套接字
相关方法
方法名
说明
Socket accept()
监听要连接到此的套接字并接受它
/* TCP接收数据的步骤 1:创建服务器端的Socket对象(ServerSocket) 2:获取输入流,读数据,并把数据显示在控制台 3:释放资源 */ public class ServerReceive { public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket serverSocket = new ServerSocket(10010); //Socket accept() 侦听要连接到此套接字并接受它 Socket accept = serverSocket.accept(); //获取输入流,读数据,并把数据显示在控制台 InputStream inputStream = accept.getInputStream(); byte[] bytes = new byte[1024]; int len = inputStream.read(bytes); String data = new String(bytes, 0, len); System.out.println("传输内容是: " + data); //释放资源 //inputStream.close(); serverSocket.close(); } }TCP通信程序练习
public class ClientTcp { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号 //Socket socket = new Socket(InetAddress.getByName("192.168.119.1"), 10010); //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 Socket socket = new Socket("192.168.119.1", 10011); //获取输出流,写数据 //OutputStream getOutputStream() 返回此套接字的输出流 OutputStream outputStream = socket.getOutputStream(); outputStream.write("TCP,我来了".getBytes()); //接收服务器端的反馈 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len = inputStream.read(bytes); System.out.println("服务器端反馈内容:" + new String(bytes, 0, len)); //释放资源 //outputStream.close(); //inputStream.close(); socket.close(); //以上两个对象都来自socket,所以释放socket,其它两个也就释放了 } } public class ServerTcp { public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket serverSocket = new ServerSocket(10011); //Socket accept() 侦听要连接到此套接字并接受它 Socket socket = serverSocket.accept(); //获取输入流,读数据,并把数据显示在控制台 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len = inputStream.read(bytes); String data = new String(bytes, 0, len); System.out.println("传输内容是: " + data); //给出反馈已收到数据 OutputStream outputStream = socket.getOutputStream(); outputStream.write("已收到数据!".getBytes()); //释放资源 //inputStream.close(); //outputStream.close(); //socket.close(); serverSocket.close(); } } /* ClientTcp 服务器端反馈内容:已收到数据! ServerTcp 传输内容是: TCP,我来了 */ public class ClientTcp { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号 //Socket socket = new Socket(InetAddress.getByName("192.168.119.1"), 10010); //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 Socket socket = new Socket("192.168.119.1", 10011); //从建盘获取输出流,写数据 //OutputStream getOutputStream() 返回此套接字的输出流 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); //封装输出流对象 //OutputStream outputStream = socket.getOutputStream(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String line; while((line = bufferedReader.readLine()) != null){ if (line.equals("886")){ break; } //发送数据\ bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //释放资源 //outputStream.close(); //inputStream.close(); socket.close(); //以上两个对象都来自socket,所以释放socket,其它两个也就释放了 } } public class ServerTcp { public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket serverSocket = new ServerSocket(10011); //Socket accept() 侦听要连接到此套接字并接受它 Socket socket = serverSocket.accept(); //获取输入流,读数据,并把数据显示在控制台 //InputStream inputStream = socket.getInputStream(); //封装输入流对象 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line; while((line=bufferedReader.readLine()) != null){ System.out.println("传输内容是: " + line); } //释放资源 //inputStream.close(); //outputStream.close(); //socket.close(); serverSocket.close(); } } public class ClientTcp { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号 //Socket socket = new Socket(InetAddress.getByName("192.168.119.1"), 10010); //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 Socket socket = new Socket("192.168.119.1", 10011); //从建盘获取输出流,写数据 //OutputStream getOutputStream() 返回此套接字的输出流 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); //封装输出流对象 //OutputStream outputStream = socket.getOutputStream(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String line; while((line = bufferedReader.readLine()) != null){ if (line.equals("886")){ break; } //发送数据\ bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //释放资源 //outputStream.close(); //inputStream.close(); socket.close(); //以上两个对象都来自socket,所以释放socket,其它两个也就释放了 } } public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket serverSocket = new ServerSocket(10011); //Socket accept() 侦听要连接到此套接字并接受它 Socket socket = serverSocket.accept(); //获取输入流,读数据,并把数据显示在控制台 //InputStream inputStream = socket.getInputStream(); //封装输入流对象 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //创建缓冲输出流对象 BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("output.txt")); String line; while((line=bufferedReader.readLine()) != null){ bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //释放资源 //inputStream.close(); //outputStream.close(); //socket.close(); serverSocket.close(); bufferedWriter.close(); } public class ClientTcp { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号 //Socket socket = new Socket(InetAddress.getByName("192.168.119.1"), 10010); //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 Socket socket = new Socket("192.168.119.1", 10011); //从文件获取数据,写数据 BufferedReader bufferedReader = new BufferedReader(new FileReader("output.txt")); //封装输出流对象 //OutputStream outputStream = socket.getOutputStream(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String line; while((line = bufferedReader.readLine()) != null){ if (line.equals("886")){ break; } //发送数据\ bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //释放资源 //outputStream.close(); //inputStream.close(); socket.close(); //以上两个对象都来自socket,所以释放socket,其它两个也就释放了 } } public class ServerTcp { public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket serverSocket = new ServerSocket(10011); //Socket accept() 侦听要连接到此套接字并接受它 Socket socket = serverSocket.accept(); //获取输入流,读数据,并把数据显示在控制台 //InputStream inputStream = socket.getInputStream(); //封装输入流对象 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //创建缓冲输出流对象 BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("input.txt")); String line; while((line=bufferedReader.readLine()) != null){ bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //释放资源 //inputStream.close(); //outputStream.close(); //socket.close(); serverSocket.close(); bufferedWriter.close(); } } public class ClientTcp { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号 //Socket socket = new Socket(InetAddress.getByName("192.168.119.1"), 10010); //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 Socket socket = new Socket("192.168.119.1", 10011); //从文件获取数据,写数据 BufferedReader bufferedReader = new BufferedReader(new FileReader("output.txt")); //封装输出流对象 //OutputStream outputStream = socket.getOutputStream(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String line; while((line = bufferedReader.readLine()) != null){ if (line.equals("886")){ break; } //发送数据\ bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //此时客户端在等待服务器端输入数据,服务器端也在等待客户端输入数据 //自动以结束标记 // bufferedWriter.write("886"); // bufferedWriter.newLine(); // bufferedWriter.flush(); //自带结束语句 socket.shutdownOutput(); //接收反馈 BufferedReader feedback = new BufferedReader(new InputStreamReader(socket.getInputStream())); System.out.println("服务器端反馈内容为:" + feedback.readLine()); //释放资源 //outputStream.close(); //inputStream.close(); feedback.close(); socket.close(); //以上两个对象都来自socket,所以释放socket,其它两个也就释放了 } } public class ServerTcp { public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket serverSocket = new ServerSocket(10011); //Socket accept() 侦听要连接到此套接字并接受它 Socket socket = serverSocket.accept(); //获取输入流,读数据,并把数据显示在控制台 //InputStream inputStream = socket.getInputStream(); //封装输入流对象 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); //创建缓冲输出流对象 BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("input.txt")); String line; while((line=bufferedReader.readLine()) != null){ // if (line.equals("886")){ // break; // } bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //此时客户端在等待服务器端输入数据,服务器端也在等待客户端输入数据 //给出反馈 BufferedWriter feedback = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); feedback.write("数据已接收完毕!"); //释放资源 //inputStream.close(); //outputStream.close(); //socket.close(); serverSocket.close(); feedback.close(); bufferedWriter.close(); } } public class ServerTcp { public static void main(String[] args) throws IOException { //创建服务器端的Socket对象(ServerSocket) //ServerSocket(int port) 创建绑定到指定端口的服务器套接字 ServerSocket serverSocket = new ServerSocket(10011); while (true) { //Socket accept() 侦听要连接到此套接字并接受它 Socket socket = serverSocket.accept(); //为每一个客户端开启一个线程 new Thread(new serverThread(socket)).start(); } //释放资源 //inputStream.close(); //outputStream.close(); //socket.close(); // serverSocket.close(); // feedback.close(); // bufferedWriter.close(); } } public class serverThread implements Runnable { private Socket socket; public serverThread() { } public serverThread(Socket socket) { this.socket = socket; } @Override public void run() { try { //接收数据写到文本文件 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); // BufferedWriter bw = new BufferedWriter(new FileWriter("myNet\\Copy.java")); //解决名称冲突问题 int count = 0; File file = new File("Copy["+count+"].java"); while (file.exists()) { count++; file = new File("Copy["+count+"].java"); } BufferedWriter bw = new BufferedWriter(new FileWriter(file)); String line; while ((line=br.readLine())!=null) { bw.write(line); bw.newLine(); bw.flush(); } //给出反馈 BufferedWriter bwServer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bwServer.write("文件上传成功"); bwServer.newLine(); bwServer.flush(); //释放资源 socket.close(); } catch (IOException e) { e.printStackTrace(); } } } public class ClientTcp { public static void main(String[] args) throws IOException { //创建客户端的Socket对象(Socket) //Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号 //Socket socket = new Socket(InetAddress.getByName("192.168.119.1"), 10010); //Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 Socket socket = new Socket("192.168.119.1", 10011); //从文件获取数据,写数据 BufferedReader bufferedReader = new BufferedReader(new FileReader("output.txt")); //封装输出流对象 //OutputStream outputStream = socket.getOutputStream(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String line; while((line = bufferedReader.readLine()) != null){ if (line.equals("886")){ break; } //发送数据\ bufferedWriter.write(line); bufferedWriter.newLine(); bufferedWriter.flush(); } //此时客户端在等待服务器端输入数据,服务器端也在等待客户端输入数据 //自动以结束标记 // bufferedWriter.write("886"); // bufferedWriter.newLine(); // bufferedWriter.flush(); //自带结束语句 socket.shutdownOutput(); //接收反馈 BufferedReader feedback = new BufferedReader(new InputStreamReader(socket.getInputStream())); System.out.println("服务器端反馈内容为:" + feedback.readLine()); //释放资源 //outputStream.close(); //inputStream.close(); feedback.close(); socket.close(); //以上两个对象都来自socket,所以释放socket,其它两个也就释放了 } }TCP协议保证数据传输可靠性的方式主要有:
(校 序 重 流 拥)
校验和:
发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
确认应答+序列号(累计确认+seq):
接收方收到报文就会确认(累积确认:对所有按序接收的数据的确认)
TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
超时重传:
当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
流量控制:
TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议。
接收方有即时窗口(滑动窗口),随ACK报文发送
拥塞控制:
当网络拥塞时,减少数据的发送。
发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口,取小
慢启动、拥塞避免、拥塞发送、快速恢复
应用数据被分割成TCP认为最适合发送的数据块。
TCP的接收端会丢弃重复的数据。
序列号:TCP传输时将每个字节的数据都进行了编号,这就是序列号。
确认应答:TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。
这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。
序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。
这也是TCP传输可靠性的保证之一。
在进行TCP传输时,由于确认应答与序列号机制,也就是说发送方发送一部分数据后,都会等待接收方发送的ACK报文,并解析ACK报文,判断数据是否传输成功。
如果发送方发送完数据后,迟迟没有等到接收方的ACK报文,这该怎么办呢?而没有收到ACK报文的原因可能是什么呢?
首先,发送方没有接收到响应的ACK报文原因可能有两点:
a、数据在传输过程中由于网络原因等直接全体丢包,接收方没有接收到。
b、接收方接收到了响应的数据,但是发送的ACK报文响应却由于网络原因丢包了。
TCP在解决这个问题的时候引入了一个新的机制,叫做超时重传机制。
简单理解就是发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。
如果是刚才第一个原因,接收方收到二次重发的数据后,便进行ACK应答。
如果是第二个原因,接收方发现接收的数据已存在(判断存在的根据就是序列号,所以上面说序列号还有去除重复数据的作用),那么直接丢弃,仍旧发送ACK应答。
那么发送方发送完毕后等待的时间是多少呢?如果这个等待的时间过长,那么会影响TCP传输的整体效率,如果等待时间过短,又会导致频繁的发送重复的包。如何权衡?
由于TCP传输时保证能够在任何环境下都有一个高性能的通信,因此这个最大超时时间(也就是等待的时间)是动态计算的。
注意:
超时以500ms(0.5秒)为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
重发一次后,仍未响应,那么等待2*500ms的时间后,再次重传。等待4*500ms的时间继续重传。以一个指数的形式增长。
累计到一定的重传次数,TCP就认为网络或者对端出现异常,强制关闭连接。
三次握手和四次挥手
接收端在接收到数据后,对其进行处理。如果发送端的发送速度太快,导致接收端的结束缓冲区很快的填充满了。此时如果发送端仍旧发送数据,那么接下来发送的数据都会丢包,继而导致丢包的一系列连锁反应,超时重传呀什么的。而TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。
简单来说就是接收方处理不过来的时候,就把窗口缩小,并把窗口值告诉发送端。
在这里只考虑A向B发送数据,假设在连接在建立时,B告诉A:我的接收窗口rwnd=400(receiver window),不过在报文中已经省略了
如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。
这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
慢开始算法原理
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
接收方有足够大的接收缓存,因此不会发生流量控制;虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
当TCP连接进行初始化是,将拥塞窗口置为1。
图中的窗口单位不再使用字节而使用报文段。
慢开始门限的初始值设置为16个报文段,即ssthresh=16;
慢开始和拥塞避免
1.然后开始慢开始算法(指数增长)。当cwnd=16时开始执行拥塞避免算法,呈现线性增长。
2.当拥塞窗口cwnd=24时出现超时,发送方判定为网络拥塞,于是调整门限值ssthresh=cwnd/2=12,同时设置拥塞窗口为1,进入慢开始阶段。
3.按照慢开始算法,发送方每收到一个新报文段的确认ACK拥塞窗口值增加。当cwnd=12时(图中点3)执行拥塞避免算法
快重传和快恢复
4 .当cwnd=16时(图中点4)出现了一个新的情况,就是发送方连续收到3个对统一报文段的重复确认(3-ACK)。发送方执行快重传和快恢复算法。
5 在图中点4,发送方知道只是丢失了个别的报文段,于是不启动慢开始,而是先进行快重传然后执行快恢复算法。
发送方设置调整门限值ssthresh=cwnd/2=8, 同时拥塞窗口cwnd=ssthresh=8(点5),然后进行拥塞避免算法
快重传:收到3个同样的确认就立刻重传,不等到超时;
快恢复:cwnd不是从1重新开始。
回到顶部
实际中的传输方式,
需要说明一下,如果你不了解TCP的滑动窗口这个事,你等于不了解TCP协议。
我们都知道,TCP必需要解决的可靠传输以及包乱序(reordering)的问题,
所以,TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。
发送方滑动窗口示意图:
上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口)
#1已收到ack确认的数据。
#2已发出但还没收到ack的。
#3在窗口中还没有发出的(接收方还有空间)。
#4窗口以外的数据(接收方没空间)
注意:
滑动窗口里是 已发出但未收到ACk、还未发出的 数据
下面是个滑动后的示意图(收到36的ack,并发出了46-51的字节):