参考资料:百度百科TCP协议
本文涉及Java IO流、异常的知识,可参考我的另外的博客
1.概述
计算机网络相关知识:
OSI七层模型
一个报文可以类似于一封信,就像下图(引自狂神说Java)非常生动。
网络编程的目的:数据交换、通信
网络通信的要素:
如何实现网络通信?
通信双方地址:
- ip
- 端口号
网络协议:
HTTP, FTP, TCP, UDP 等等
1.1 IP
IP地址:InetAddress
(无构造器)
- 唯一定位一台网络上计算机
- 127.0.0.1 :本机,localhost
- ip地址分类:ipv4(4个字节)/ipv6(128位,8个无符号整数组成),公网(ABCD类地址)/私网(局域网)
- 域名:记忆ip问题
主机名解析
主机名称到IP地址解析是通过使用本地机器配置信息和网络命名服务(如域名系统(DNS)和网络信息服务(NIS))的组合来实现的。所使用的特定命名服务是默认配置的本地机器。对于任何主机名,返回其对应的IP地址。反向名称解析意味着对于任何IP地址,返回与IP地址关联的主机。
InetAddress
类提供了将主机名解析为其IP地址的方法,反之亦然。
InetAddress常用方法:
举例:
public static void main(String[] args) throws UnknownHostException {
//查询本机地址
InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
System.out.println(inetAddress);
InetAddress i1 = InetAddress.getByName("localhost");
System.out.println(i1);
InetAddress i2 = InetAddress.getLocalHost();
System.out.println(i2);
//查询网站ip
InetAddress i3 = InetAddress.getByName("www.baidu.com");
System.out.println(i3);
//常用方法
System.out.println(i3.getAddress()); //返回的是byte[],所以输出了乱码
System.out.println(i3.getCanonicalHostName());//规范的名字
System.out.println(i3.getHostAddress());//ip
System.out.println(i3.getHostName());//主机名
}
InetAddress
没有构造器,所以需要调用静态方法进行构造。上述代码结果为:
1.2 端口
端口表示计算机上的一个程序的进程
- 不同的进程有不同的端口号,用来区分软件。
- 一般被规定为0~65535
- TCP端口和UDP端口,均有65536个,两个互不冲突。单个协议下端口是不能冲突的,例:TCP占用8080后,不能再次占用此TCP端口了
- 端口分类:公有端口049151,用来分配给用户或者程序,Tomcat:8080,MySQL:3306,Oracle:1521。动态、私有:49152~65535,尽量不要用这里的端口。
netstat -ano #这条命令用于查看所有端口
netstat -ano|findstr "8080" #查看指定的端口
tasklist|findstr "8696" #查看指定端口的进程
以上均为Linux命令
InetSocketAddress
该类实现IP套接字地址(IP地址+端口号)它也可以是一对(主机名+端口号),在这种情况下将尝试解析主机名。如果解决方案失败,那么该地址被认为是未解决的,但在某些情况下仍可以使用,例如通过代理连接。
它提供了用于绑定,连接或返回值的套接字所使用的不可变对象。
通配符是一个特殊的本地IP地址。 通常意味着“任何”,只能用于bind
操作。
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8080);
InetSocketAddress inetSocketAddress1 = new InetSocketAddress("localhost",8080);
System.out.println(inetSocketAddress);
System.out.println(inetSocketAddress1);
System.out.println(inetSocketAddress.getAddress());
System.out.println(inetSocketAddress.getHostName());
System.out.println(inetSocketAddress.getPort());
以上为相关代码。
1.3 通信协议
网络通信协议可能涉及到:速率,传输码率,代码结构,传输控制等等
主要涉及的是以下两个:
TCP:用户传输协议(3次握手,确定返回信息,以后网络相关知识具体说,不在本篇赘述)
UDP:用户数据报协议(不确定返回信息)
TCP和UDP对比
TCP就像打电话,需要连接,稳定
UDP就像发短信,不需要连接,发完即结束,不稳定
基于连接与无连接;
对系统资源的要求(TCP较多,UDP少);
UDP程序结构较简单;
流模式与数据报模式 ;(从下面demo中即可看出)
TCP保证数据正确性,UDP可能丢包;
TCP保证数据顺序,UDP不保证。
2. TCP协议
TCP三次握手的过程如下:
- 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
- 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入
SYN_RECV
状态。 - 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入
Established
状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
TCP连接终止过程:
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如上图所示。
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 [3]
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
TCP协议相关资料参考自百度百科,计算机网络相关知识不再详细描述。
2.1 TCP连接的实现
服务端:
public class TestTCPserver {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
Socket accept = serverSocket.accept();//等待client连接
InputStream is = accept.getInputStream();
//管道流,将一个输入流通过管道转化为一个合适的输出流,不用管道流直接String可能会输出乱码
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while((len=is.read(buffer))!=-1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
baos.close();
is.close();
accept.close();
serverSocket.close();
//正式写代码的过程中,一定要用try,catch,finally,为了代码的安全,出了事故容易判断
}
}
用到了socket
类,其中涉及了IO流部分的知识。
客户端:
public class TestTCPclient {
public static void main(String[] args) throws IOException {
InetAddress serverIp = InetAddress.getByName("127.0.0.1");
int port = 9999;
//创建一个socket连接,连接的是本机
Socket socket = new Socket(serverIp,port);
OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
os.close();
socket.close();
}
}
注意:socket
是需要关闭的
socket
该类实现客户端套接字(也称为“套接字”)。套接字是两台机器之间通讯的端点。套接字的实际工作由SocketImpl
类的实例执行。 应用程序通过更改创建套接字实现的套接字工厂,可以配置自己创建适合本地防火墙的套接字。ServerSocket
该类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。
服务器套接字的实际工作由SocketImpl
类的实例执行。 应用程序可以更改创建套接字实现的套接字工厂,以配置自己创建适合本地防火墙的套接字。
客户端:连接服务器socket
,发送消息
服务器:建立服务的端口 ServerSocket
,等待用户连接,接受用户消息
2.1 TCP实现文件上传
与消息传递类似,只是IO操作稍微变了一下
服务端:
public class TCPserverdemo1 {
public static void main(String[] args) throws IOException {
//创建服务
ServerSocket serverSocket = new ServerSocket(9000);
//监听客户端的连接
Socket accept = serverSocket.accept(); //阻塞式监听,会一定等待客户端连接
InputStream is = accept.getInputStream();
FileOutputStream fos = new FileOutputStream(new File("copide1.jpg"));
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close();
is.close();
accept.close();
serverSocket.close();
}
}
客户端:
public class TCPclientdemo1 {
public static void main(String[] args) throws IOException {
//创建一个socket连接
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9000);
//创建一个输出流
OutputStream os = socket.getOutputStream(); //.getOutputStream获得了一个SocketOutputStream实例
//读取文件
FileInputStream fis = new FileInputStream(new File("image.jpg"));
byte[] buffer = new byte[1024];
int len;
while((len=fis.read(buffer))!=-1){
os.write(buffer,0,len); //涉及到BIO
}
fis.close();
os.close();
socket.close();
}
}
上面用的是字节流,字节缓冲流也可以使用。字符流不可以,因为可能会读其他文件的类型,字节流比较稳妥。
服务器接收完信息后其实是可以返回消息到客户端的,socket
通信:
服务端可增加:
accept.shutdownInput();
//通知客户端已经接收完毕
OutputStream os = accept.getOutputStream();
os.write("ending".getBytes()); //发送给客户端
客户端可增加:
socket.shutdownOutput();//通知服务器传输完毕
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while((len2 = is.read(buffer))!=-1){
baos.write(buffer,0,len2);
}
System.out.println(baos);
客户端读入了服务端返回的ending
字符串。
实现两端通信之后再决定是否进行其他操作,例如close()
等等。
3. UDP
无需连接,但是需要知道对方地址
3.1 UDP消息发送
主要依赖的是DatagramSocket
和DatagramPacket
DatagramSocket
此类表示用于发送和接收数据报数据包的套接字。 数据报套接字是分组传送服务的发送或接收点。 在数据报套接字上发送或接收的每个数据包都被单独寻址和路由。 从一个机器发送到另一个机器的多个分组可以不同地路由,并且可以以任何顺序到达。 在可能的情况下,新构建的DatagramSocket
启用了SO_BROADCAST套接字选项,以允许广播数据报的传输。 为了接收广播数据包,DatagramSocket
应该绑定到通配符地址。 在一些实现中,当DatagramSocket
绑定到更具体的地址时,也可以接收广播分组。DatagramPacket
该类表示数据报包。 数据报包用于实现无连接分组传送服务。 仅基于该数据包中包含的信息,每个消息从一台机器路由到另一台机器。 从一台机器发送到另一台机器的多个分组可能会有不同的路由,并且可能以任何顺序到达。 包传送不能保证。
以下实现UDP消息传送的一个简单例子:
public class TestUDP1 {
//不需要连接服务器
public static void main(String[] args) throws IOException {
//建立Socket
DatagramSocket datagramSocket = new DatagramSocket(); //为空将默认绑定一个可用端口
//建个数据报
String message = "hello,server";
InetAddress inetAddress = InetAddress.getByName("localhost");
int port = 9000;
int len = message.getBytes().length;
//数据,数据的长度起始位置,要发送给谁
DatagramPacket datagramPacket = new DatagramPacket(message.getBytes(), 0,len, inetAddress, port);
datagramSocket.send(datagramPacket); //进行发送
datagramSocket.close();
}
}
UDP发送完就不需要其他操作了,为了验证我们发送的消息,建立了一个接收端:
public class UDPaccept {
public static void main(String[] args) throws IOException {
//开放端口
DatagramSocket socket = new DatagramSocket(9000);
//接收数据报
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length); //接收并不需要对方地址和端口
socket.receive(packet); //阻塞式接收
System.out.println(packet.getAddress());
System.out.println(new String(packet.getData(),0,packet.getData().length));
socket.close();
}
}
其中收发消息用到了两个方法DatagramSocket.send()
和DatagramSocket.receive()
3.2 UDP聊天的实现(单向)
通过UDP协议进行一个发送端和接收端的demo。当出现"bye"时,结束对话。
发送端:
public class UDPsender {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(8080);//发送端口
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
//BufferedInputStream bis = new BufferedInputStream(System.in);
int len;
while(true){
String data = reader.readLine(); //接收键盘输入的信息
DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress("localhost",6000));
socket.send(packet);
if(data.equals("bye"))
break;
}
reader.close();
socket.close();
}
}
接收端:
public class UDPreceiver {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(6000);//打开接收端口
while(true){
byte[] container = new byte[1024]; //用来装数据报内容
DatagramPacket packet = new DatagramPacket(container,0,container.length); //接受时候只需要一个空byte[]
socket.receive(packet);
byte[]data = packet.getData();
String datas = new String(data,0,data.length); //仍带有byte[]的其他信息,若转为真正字符串需trim()
System.out.println(datas);
if(datas.trim().equals("bye"))
break;
}
socket.close();
}
}
这个demo只实现了单向发消息。后续将实现双向发送。
3.3 双向聊天(多线程)
双向聊天和以上内容相似,只需要每个端开启两个线程(接收线程和发送线程),以下为代码演示:
public class TalkSend implements Runnable{ //发送线程
DatagramSocket socket = null;
BufferedReader reader = null;
private String ToIP;
private int ToPort;
public TalkSend(String toString, int toPort) {
ToIP = toString;
ToPort = toPort;
}
@Override
public void run() {
try {
socket = new DatagramSocket();//发送端口
reader = new BufferedReader(new InputStreamReader(System.in));
} catch (SocketException e) {
e.printStackTrace();
}
//BufferedInputStream bis = new BufferedInputStream(System.in);
while(true){
String data = null;
try {
data = reader.readLine();
DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress(this.ToIP,this.ToPort));
socket.send(packet);
if(data.equals("bye"))
break;
} catch (IOException e) {
e.printStackTrace();
}
}
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
socket.close();
}
}
public class TalkReceive implements Runnable{ //接收线程
DatagramSocket socket = null;
private int FromPort;
private String Person;
public TalkReceive(int fromPort,String person) {
FromPort = fromPort;
Person = person;
}
@Override
public void run() {
try {
socket = new DatagramSocket(this.FromPort);//打开接收端口
} catch (SocketException e) {
e.printStackTrace();
}
while(true){
byte[] container = new byte[1024]; //用来装数据报内容
DatagramPacket packet = new DatagramPacket(container,0,container.length);
try {
socket.receive(packet);
} catch (IOException e) {
e.printStackTrace();
}
byte[]data = packet.getData();
String datas = new String(data,0,data.length);
System.out.println(Person+":"+datas);
if(datas.trim().equals("bye"))
break;
}
socket.close();
}
}
然后需要设置两个端进行聊天:
new Thread(new TalkSend("localhost",8080)).start();
new Thread(new TalkReceive(6000,"老师")).start();
这里设置为学生端,然后开启两个线程,设置发送端口和接收端口
new Thread(new TalkSend("localhost",6000)).start();
new Thread(new TalkReceive(8080,"学生")).start();
这里设置为老师端,然后开启线程,设置端口,其实打开了4个端口,因为学生端和老师端发送线程中,DatagramSocket
默认绑定的还有两个端口。
4. URL
统一资源定位符:定位互联网上的某个资源
DNS域名解析: www.baidu.com ——》xxx.xxx.xxx.xxx
协议://ip地址:端口/项目名/资源
URL类的基本使用
URL url = new URL("http://localhost:8080/helloworld/index.jsp?username=yuan&password=123");
System.out.println(url.getProtocol());//得到协议名
System.out.println(url.getHost());//主机
System.out.println(url.getPort());//端口
System.out.println(url.getPath());//文件
System.out.println(url.getFile());//文件全路径
System.out.println(url.getQuery()); //得到url查询的部分(参数)
Java万物皆对象
下载一个URL资源
public class UrlDown {
public static void main(String[] args) throws IOException {
URL url = new URL("http://localhost:8080/gaoyuan/SecurityFile.txt");
//连接到这个资源
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); //需要转换类型,因为返回的是URPConnection
InputStream is = urlConnection.getInputStream();
FileOutputStream fos = new FileOutputStream("SecurityFile.txt");
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
fos.close();
is.close();
urlConnection.disconnect();//断开连接
}
}
这是打开Tomcat
服务器后,把一个文件添加到相应目录之后下载的。
网络编程基础部分结束,例如:计算机网络,BIO等知识将会在后续博客中发布。