TCP/IP网络模型
网络通信协议有很多种,目前应用最广泛的是TCP/IP协议(Transmission Control Protocal/Internet Protoal传输控制协议/英特网互联协议),它是一个包括TCP协议和IP协议,UDP(User Datagram Protocol)协议和其它一些协议的协议组,在学习具体协议之前首先了解一下TCP/IP协议组的层次结构。
链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。
IP地址和端口号
要想使网络中的计算机能够进行通信,必须为每台计算机指定一个标识号,通过这个标识号来指定接受数据的计算机或者发送数据的计算机。
在TCP/IP协议中,这个标识号就是IP地址,它可以唯一标识一台计算机,目前,IP地址广泛使用的版本是IPv4,它是由4个字节大小的二进制数来表示,如:00001010000000000000000000000001。由于二进制形式表示的IP地址非常不便记忆和处理,因此通常会将IP地址写成十进制的形式,每个字节用一个十进制数字(0-255)表示,数字间用符号“.”分开,如 “192.168.1.100”。
随着计算机网络规模的不断扩大,对IP地址的需求也越来越多,IPV4这种用4个字节表示的IP地址面临枯竭,因此IPv6 便应运而生了,IPv6使用16个字节表示IP地址,它所拥有的地址容量约是IPv4的8×1028倍,达到2128个(算上全零的),这样就解决了网络地址资源数量不够的问题。
本机的回环ip,相当于汉语中的"我"
127.0.0.1这个ip就表示当前主机的ip
端口号的作用:
通过IP地址可以连接到指定计算机,但如果想访问目标计算机中的某个应用程序,还需要指定端口号。
端口号是操作系统自动分配的,是逻辑端口。
在计算机中,不同的应用程序是通过端口号区分的。端口号是用两个字节(16位的二进制数)表示的,它的取值范围是065535**,其中,**01023之间的端口号用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上的端口号,从而避免端口号被另外一个应用或服务所占用。
冒号后的5000代表端口号
端口号和IP地址的作用示意图
注:每个端口号只能代表一个应用程序
当我们用http://www.baidu.com来访问的时候,这个网址也相当于一个IP地址,其实网址后跟着一个默认端口号80
所以完整网址因该是http://www.baidu.com:80
通俗来讲,一个网址也相当于一个主机名
使用java编码获取本机ip地址
使用java.net.InetAddress该类的getLocalHost 静态方法获取本地主机的ip地址
public class InetAdress {
public static void main(String[] args) throws UnknownHostException {
function();
}
public static void function() throws UnknownHostException
{
InetAddress inet = InetAddress.getLocalHost();
System.out.println(inet);
}
}
使用splite将字符串分割,获得分别的主机名和ip地址
public class InetAdress {
public static void main(String[] args) throws UnknownHostException {
function();
}
public static void function() throws UnknownHostException
{
InetAddress inet = InetAddress.getLocalHost();
String host = inet.toString();
String[] splite = host.split("/");
for(String i :splite)
{
System.out.println(i);
}
}
}
这个分割主机和ip地址的活,sun公司已经帮你做好了
非静态方法
getHostAddress()
getHostName()
public class InetAdress {
public static void main(String[] args) throws UnknownHostException {
function();
}
public static void function() throws UnknownHostException
{
InetAddress inet = InetAddress.getLocalHost();
System.out.println(inet.getHostAddress());
System.out.println(inet.getHostName());
}
}
总结一下:以下四种方法
1:可以通过别人的主机名来获得InetAdress类对象
2:获取本机的InetAdress类对象
3:根据InetAdress类对象获取主机名
4:根据InetAdress类对象获取主机IP地址
运输层的两个重要协议: TCP UDP
UDP协议
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
TCP协议
TCP协议是面向连接的通信协议,即在传输数据前先在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。第一次握手,客户端向服务器端发出连接请求,等待服务器确认,第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求,第三次握手,客户端再次向服务器端发送确认信息,确认连接。
UDP通信代码实现
两个重要的类
DatagramPacket()和DatagramSocket()
DatagramPacket()负责打包数据
DatagramSocket()负责运输数据
发送方
- 实现步骤:
- 创建DatagramPacket(),封装数据,IP地址和端口号
- 创建DatagramSocket()对象
- 调用DatagramSocket()对象的send方法,发送封装后的数据
- 调用DatagramSocket()对象可以发送也可以接收
- 关闭资源
public class udpsend {
public static void main(String[] args) throws IOException {
//创建DatagramPacket()对象,封装数据,IP地址和端口号
byte[] data = "你好UDP".getBytes();
//创建InetAddress对象,封装自己的IP地址
//但是封装的IP地址是目标地址
InetAddress inet = InetAddress.getByName("10.7.63.160");
//最后一个参数是端口号 ,6000端口
DatagramPacket np = new DatagramPacket(data, data.length, inet,6000);
//套接字socket定义:绑定了Ip地址和端口号的网络对象
DatagramSocket ds = new DatagramSocket();
ds.send(np);
//关闭资源
ds.close();
}
}
接收方
/*
1。创建DatagramSocket(),绑定端口号,与发送端口保持一致
2。创建字节数组,接收发来的消息
3。创建DatagramPacket()对象
4。调用DatagramSocket()的receive()
receive(DatagramPacket dp)
作用是将接收的字节数组的数据放在dp包中
5。拆包
得到
发送方的IP地址
DatagramPacket对象中的方法getAddress()
返回值是InetAddress类的对象
接收到的字节个数
DatagramPacket对象中的方法getLength()
发送方的端口号
DatagramPacket对象中的方法.getPort()
该端口号一般是操作系统自动指派的,一般没什么意义
6。关闭资源
*/
public class udpreceive {
public static void main(String[] args) throws IOException {
//创建数据包传输对象ds,绑定端口号
DatagramSocket ds = new DatagramSocket(6000);
//创建字节数组
byte[] data = new byte[1024];
//创建数据包对象,传递字节数组
DatagramPacket dp = new DatagramPacket(data, data.length);
//调用ds对象的receive方法来接收数据
ds.receive(dp);
int port = dp.getPort();
InetAddress address = dp.getAddress();
String ip_address = address.getHostAddress();
int length = dp.getLength();
//通过String类将data对象字符串化
System.out.println(new String(data,0,length)+"ip地址"+ip_address+"端口号"+port);
ds.close();
}
}
将其改为聊天小程序
先打开接收端,再打开发送端
最后一个小三角图标是选择控制台的
发送端
public class udpreceive {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(6000);
byte[] data = new byte[1024];
//不将数组放入循环,不然太浪费了
//我之前想会不会数组的数据没有清除,导致输出错误
//下面的new String(data,0,length),会输出指定长度的字符串
while(true)
{
DatagramPacket dp = new DatagramPacket(data, data.length);
ds.receive(dp);
int length = dp.getLength();
System.out.println(new String(data,0,length));
}
}
}
接收端
public class udpreceive {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(6000);
byte[] data = new byte[1024];
while(true)
{
DatagramPacket dp = new DatagramPacket(data, data.length);
ds.receive(dp);
int length = dp.getLength();
System.out.println(new String(data,0,length));
}
}
}
TCP通信
- TCP通信同UDP通信一样,都能实现两台计算机之间的通信,通信的两端都需要创建socket对象。
- 区别在于,UDP中只有发送端和接收端,不区分客户端与服务器端,计算机之间可以任意地发送数据。
- 而TCP通信是严格区分客户端与服务器端的,在通信时,必须先由客户端去连接服务器端才能实现通信,服务器端不可以主动连接客户端,并且服务器端程序需要事先启动,等待客户端的连接。
- 在JDK中提供了两个类用于实现TCP程序,一个是ServerSocket类,用于表示服务器端,一个是Socket类,用于表示客户端。
- 通信时,首先创建代表服务器端的ServerSocket对象,该对象相当于开启一个服务,并等待客户端的连接,然后创建代表客户端的Socket对象向服务器端发出连接请求,服务器端响应请求,两者建立连接开始通信。
- 客户端和服务器交换一次数据需要四次IO流,大体过程是客户端发送abc,服务器接收abc,服务器发送"你好",客户端接收"你好"。
客户端程序:
-
实现tcp客户端 使用java.net.Socket类
-
构造方法:
Socket(String host, int port)
传递服务器的IP和端口号
构造方法只要运行,就开始连接服务器,若连接失败,则会抛出异常
和服务器实现数据交换 -
OutputStream getOutputStream() 返回套接字的输出流
作用:将数据流输出,输出到服务器 -
InputStream getInputStream() 返回套接字的输入流
作用:从服务器读取数据 -
这两个流对象是客户端和服务器在建立链接时就已经自动生成的
我们只需在建立的socket对象中得到这输入输出流对象即可,只能用这两个对象进行客户机和服务器之间的交换
public class tcpclient {
public static void main(String[] args) throws IOException{
// 创建socket对象,连接服务器
Socket so = new Socket("10.7.63.160", 8888);
//通过socket对象获取输出流
OutputStream outs = so.getOutputStream();
outs.write("服务器".getBytes());
//读取服务器发回的数字,使用socket套接字中的对象输入流
InputStream in = so.getInputStream();
//新建字节数组
byte[] data = new byte[1024];
int length = in.read(data);
System.out.println(new String(data,0,length));
so.close();
}
}
服务器端程序
-
实现tcp服务器
-
表示服务器的类java.net.ServerSocket
-
构造方法:
ServerSocket(int port) -
必须要获得客户端的套接字对象socket,这样才能知道自己和哪个客户端连接了
Socket accept() -
服务器端本身没有流功能,他的功能要靠客户端的socket来获取
public class tcpserver {
public static void main(String[] args) throws IOException {
ServerSocket ser = new ServerSocket(8888);
//调用服务器套接字对象中的accept(),方法获得客户端套接字对象
Socket socket = ser.accept();
//靠客户端的套接字对象获取字节输出流,读的是客户端发来的数据
InputStream in = socket.getInputStream();
//将输入流获取的数据存放在获取的数组中
byte[] data = new byte[1024];
// int read()方法返回读入缓冲区的总字节数
int len = in.read(data);
System.out.println(new String(data,0,len));
OutputStream out = socket.getOutputStream();
out.write("收到,谢谢".getBytes());
socket.close();
ser.close();
}
}
文件上传案例
-
从客户端上传一个图片,上传到服务器中
-
服务器将图片保存到一个文件夹中
-
客户端收到服务器的上传成功反馈
客户端代码 -
实现tcp图片上传服务器
-
实现步骤:
1:使用socket套接字链接服务器 2:使用socket获取字节输出流,写图片 3:使用自己的流对象,获取图片数据源 FileInputStream 4: 读取图片,使用字节输出流,将图片写到服务器,采用字节数组进行缓冲 5:使用socket套接字获取字节输入流,读取服务器返回的上传成功信息 6:关闭资源
-
重大发现:
文件输入流(FileInputStream)和文件输出流(FileOutputStream)的输入和输出是相对于缓冲数组
文件输入流(FileInputStream)向缓冲数组中写入
文件输出流(FileOutputStream)从缓冲数组中读出
输入,就像相当于来了,来了就得读
输出,就相当于走了,走了就得写
public class tcpclient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("10.7.63.160",8000);
OutputStream out = socket.getOutputStream();
String filename = "F:\\照片\\test.jpg";
FileInputStream fis = new FileInputStream(filename);
int length = 0;
byte[] pic = new byte[1024];
while((length = fis.read(pic))!=-1)
{
out.write(pic, 0, length);
}
InputStream in = socket.getInputStream();
in.read(pic);
System.out.println(new String(pic,0,pic.length));
fis.close();
socket.close();
}
}
服务器端代码
-
tcp图片上传服务器
1:serversocket套接字对象,端口8000 2:accept对象获取客户端的连接对象 3:socket对象获得字节输入流 4: 客户端连接对象获取字节输入流,读取客户端发送的文件 5:创建file对象,绑定上传文件夹 判断文件夹是否存在,不存在,则创建文件夹 6:创建字节输出流,数据目的是file对象所在文件夹 7:字节流读取图片,写入文件夹中 8:给客户端发送成功消息 9:关闭资源
public class tcpserver {
public static void main(String[] args) throws IOException {
ServerSocket ser = new ServerSocket(8000);
Socket socket = ser.accept();
InputStream in = socket.getInputStream();
String file = "E:\\迅雷下载";
File upload = new File(file);
if(!upload.exists())
{
upload.mkdirs();
}
FileOutputStream fos = new FileOutputStream(upload+"\\1.jpg");
byte[] data = new byte[1024];
int length = 0;
while((length = in.read(data))!=-1)
{
fos.write(data,0,length);
}
OutputStream out = socket.getOutputStream();
data = "图片已经上传完毕".getBytes();
out.write(data,0,data.length);
fos.close();
socket.close();
ser.close();
}
}
但是运行后有缺陷
现象是客户端和服务器均不停止运行,且客户端没有显示上传成功字样
原因:
客户端代码段
while((length = fis.read(pic))!=-1)
{
out.write(pic, 0, length);
}
服务器代码段
while((length = in.read(data))!=-1)
{
fos.write(data,0,length);
}
客户端的fis.read(pic)读的是文件,可以遇到fis.read(pic)==-1的时候
服务器的in.read(data))读的是服务器,永远也读不到-1,所以循环不会结束,还在等
可是客户端数据已经发完了,但是服务器还在等
解决方法
Socket类中的shutdownOutput()方法
public class tcpclient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("10.7.63.160",8000);
OutputStream out = socket.getOutputStream();
String filename = "F:\\照片\\test.jpg";
FileInputStream fis = new FileInputStream(filename);
int length = 0;
byte[] pic = new byte[1024];
while((length = fis.read(pic))!=-1)
{
out.write(pic, 0, length);
}
//给服务器发送终止序列,也就是告诉服务器,我数据发完了,你别等了
socket.shutdownOutput();
InputStream in = socket.getInputStream();
int len = in.read(pic);
System.out.println(new String(pic,0,len));
fis.close();
socket.close();
}
}
为了解决同名文件被覆盖问题
public class tcpserver {
public static void main(String[] args) throws IOException {
ServerSocket ser = new ServerSocket(8000);
Socket socket = ser.accept();
InputStream in = socket.getInputStream();
String file = "E:\\下载";
File upload = new File(file);
if(!upload.exists())
{
upload.mkdirs();
}
String filename = "sun"+System.currentTimeMillis()+new Random().nextInt(999)+".jpg";
FileOutputStream fos = new FileOutputStream(upload+File.separator+filename);
byte[] data = new byte[1024];
int length = 0;
while((length = in.read(data))!=-1)
{
fos.write(data,0,length);
}
OutputStream out = socket.getOutputStream();
out.write("图片已经上传完毕".getBytes());
fos.close();
socket.close();
ser.close();
}
}
- System.currentTimeMillis()获取时间原点到现在的毫秒数
- new Random().nextInt(999)获取[0,999)之间的随机数
- File.separator 代替文件路径中的分号
- 与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 separatorChar
- 在 Microsoft Windows 系统上,它为 ‘\’
String filename = "sun"+System.currentTimeMillis()+new Random().nextInt(999)+".jpg";
FileOutputStream fos = new FileOutputStream(upload+File.separator+filename);
实现多个客户端同时上传图片
用到了多线程的思想
就是有一个客户端向服务器上传数据,服务器就开一个线程,将上传的图片保存。
所以要更改服务器端的代码
服务器线程代码
public class server {
public static void main(String[] args) throws IOException {
ServerSocket ser = new ServerSocket(8000);
while(true)
{
Socket socket = ser.accept();
//s是接口类实现对象
serverthread s = new serverthread(socket);
new Thread(s).start();
}
}
}
多线程服务器封装
public class serverthread implements Runnable{
private Socket socket;
public serverthread(Socket socket)
{
this.socket = socket;
}
@Override
public void run() {
try{
InputStream in = socket.getInputStream();
String file = "E:\\下载";
File upload = new File(file);
if(!upload.exists())
{
upload.mkdirs();
}
String filename = "sun"+System.currentTimeMillis()+new Random().nextInt(999)+".jpg";
FileOutputStream fos = new FileOutputStream(upload+File.separator+filename);
byte[] data = new byte[1024];
int length = 0;
while((length = in.read(data))!=-1)
{
fos.write(data,0,length);
}
OutputStream out = socket.getOutputStream();
out.write("图片已经上传完毕".getBytes());
fos.close();
socket.close();
}catch(Exception e) {e.printStackTrace();}
}
}
public class tcpclient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("10.7.63.160",8000);
OutputStream out = socket.getOutputStream();
String filename = "F:\\照片\\test.jpg";
FileInputStream fis = new FileInputStream(filename);
int length = 0;
byte[] pic = new byte[1024];
while((length = fis.read(pic))!=-1)
{
out.write(pic, 0, length);
}
//给服务器发送终止序列,也就是告诉服务器,我数据发完了,你别等了
socket.shutdownOutput();
InputStream in = socket.getInputStream();
int len = in.read(pic);
System.out.println(new String(pic,0,len));
fis.close();
socket.close();
}
}