说明
更新时间:2020/12/01 17:17,更新完基本内容
java的socket编程做一个总结,主要是面向Tcp和Udp编程
注意:本文仅为记录学习轨迹,如有侵权,联系删除
一、TCP/IP协议模型
在学习socket编程之前有必要复习一下TCP/IP协议模型,模型图片如下,注意,OSI是国际定义的协议,但是现在在用的协议是TCP/IP协议,所以重点学习TCP/IP协议
注意不同层之间所支持的协议,例如目前最常用的http协议等
java的socket编程,应用层采用http协议,传输层主要有TCP和UDP,网络层采用IP协议,至于链路层就不在考虑范围了,因为链路层是物理链路层面的东西,不是软件开发的层面。
注意,这里的socket编程主要是针对TCP和UDP这两个传输层协议进行的编程开发。
二、获取IP相关信息
在对TCP和UDP进行编程开发之前,先了解一下java对socket编程的支持,主要有两个类InetAddress和InetSocketAddress。
InetAddress
InetAddress是java.net包提供的用于网络编程的类,InetAddress可以获取对应ip或者对应域名的相关信息,例如ip信息和域名信息,但是没有端口号信息,如果要获取端口号信息,则需要用到下面要用到的InetSocketAddress类
public class InetAddressTest {
/**
* 获取本地主机ip信息
*/
static void getLocalhost(){
try {
//获取本地主机的相关信息
InetAddress localHost = InetAddress.getLocalHost();
//ip
String hostAddress = localHost.getHostAddress();
//主机名称
String hostName = localHost.getHostName();
System.out.println("hostAddress = " + hostAddress);
System.out.println("hostName = " + hostName);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
/**
* 或者对应的ip信息,根据传入的ip或域名获取相关信息
* @param address
*/
static void getAddress(String address){
try {
//获取对应ip的相关信息
InetAddress inetAddress = InetAddress.getByName(address);
//获取主机ip
String hostAddress = inetAddress.getHostAddress();
//获取主机名称
String hostName = inetAddress.getHostName();
System.out.println("hostAddress = " + hostAddress);
System.out.println("hostName = " + hostName);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// getLocalhost();
getAddress("www.baidu.com");
}
}
InetSocketAddress
InetSocketAddress是java.net包提供的用于网络编程的类,InetSocketAddress包含端口,用于socket通信的,下面只是演示一下一些基本功能,还没有用到通信
public class InetSocketAddressTest {
public static void main(String[] args) {
try {
//解析域名获取ip与主机信息
InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
//端口号
int port = 4321;
//获取InetSocketAddress类,主要用于socket通信,下面只是演示一下一些基本功能,还没有用到通信
InetSocketAddress socket = new InetSocketAddress(inetAddress, port);
//获取主机域名/ip
InetAddress address = socket.getAddress();
//获取主机名称,也就是域名
String hostString = socket.getHostString();
//获取主机名称,也就是域名
String hostName = socket.getHostName();
//获取端口号
int port1 = socket.getPort();
System.out.println("address = " + address);
System.out.println("hostString = " + hostString);
System.out.println("port1 = " + port1);
System.out.println("hostName = " + hostName);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
URL
这个也是java.net包里面提供的一个类,它可以通过传入的url解析里面的所有的所有url相关的信息,例如:ip、主机域名、uri、端口号等
public class UrlTest {
public static void main(String[] args) throws IOException {
String urlStr = "https://www.csdn.net/nav/java?spm=1000.2115.3001.4125";
URL url = new URL(urlStr);
//用到的应用层协议
String protocol = url.getProtocol();
//主机名称,也就是域名
String host = url.getHost();
//端口号,如果url没有显示的添加有端口号则返回-1,注意与url.getDefaultPort()q区分开来
int port1 = url.getPort();
//端口号,会自动获取默认端口号
int port2 = url.getDefaultPort();
//资源路径
String path = url.getPath();
//请求参数,get才有
String query = url.getQuery();
//获取uri
String uri = url.getFile();
System.out.println("urlStr = " + urlStr);
System.out.println("url.getProtocol() = " + url.getProtocol());
System.out.println("url.getHost() = " + url.getHost());
System.out.println("url.getPort() = " + url.getPort());
System.out.println("url.getDefaultPort() = " + url.getDefaultPort());
System.out.println("url.getPath() = " + url.getPath());
System.out.println("url.getQuery() = " + url.getQuery());
System.out.println("url.getRef() = " + url.getRef());
System.out.println("url.getAuthority() = " + url.getAuthority());
System.out.println("url.getContent() = " + url.getContent());
System.out.println("url.getFile() = " + url.getFile());
}
}
三、TCP编程
前面说过,java的socket编程主要是在传输层进行编程,即传输层的TCP和UDP协议,利用jdk提供的类进行编程,下面实现一个利用TCP协议的客户端与服务端两端通信的例子。
注意:TCP只能实现一对一的通讯,因为TCP的性质决定的,在java中的实现步骤如下
- 服务端或客户端通过Socke或者ServerSocket创建一个socket
- 两端之间通信采用流的方式,通过socket.get的方式可以得到一个输入流和输出流
- 各个端之间的信息接收与发送都是用建立的输出流或输入流进行接收与发送
- 最后记得关闭资源即可
客户端代码Client
public class Client {
public static void main(String[] args) {
Socket socket = null;
try {
//主机ip
String host = "127.0.0.1";
//主机端口号
int port = 8888;
//创建套接字,套接字是传输层Tcp像应用层Http开的一个编程接口,开发人员主要是通过套接字对tcp进行编程
socket = new Socket(host,8888);
//向服务端发起一个请求,通过socket创建io输出流
OutputStream outputStream = socket.getOutputStream();
//通过io输出流创建数据输出流
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
//发起请求,这里直接传了一个hello过去
dataOutputStream.writeUTF("这里是客户端,收到请回复");
//通过socket创建io输入流
InputStream inputStream = socket.getInputStream();
//通过io输入流创建数据输入流
DataInputStream dataInputStream = new DataInputStream(inputStream);
//接收服务端的响应
String s = dataInputStream.readUTF();
System.out.println("客户端接收到的数据:[ " + s + " ]");
//关闭数据传输流
dataOutputStream.close();
dataInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端Server
public class Server {
public static void main(String[] args) {
int port = 8888;
try {
//创建一个ServerSocket,负责服务端这边监听对应端口号
ServerSocket serverSocket = new ServerSocket(port);
//接收客户端的请求,在没接收到请求会一直处于监听的状态
Socket socket = serverSocket.accept();
//处理客户端的请求,通过socket创建io输入流
InputStream inputStream = socket.getInputStream();
//通过io输入流创建数据传输流
DataInputStream dataInputStream = new DataInputStream(inputStream);
//获取请求数据
String s = dataInputStream.readUTF();
System.out.println("服务端接收到的数据:[ " + s+" ]");
//给客户端回写数据
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
//给客户端响应数据
dataOutputStream.writeUTF("这里是服务端,已收到客户端的请求的数据");
//关闭资源
dataInputStream.close();
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
四、UDP编程
UDP跟TCP差别挺大的,TCP通信前需要进行3次握手,而且是面向连接的,只能一对一的通信,通过流的方式进行数据的接收与发送,UDP是无连接的协议,通信通过数据包的方式,不要经过3次握手,直接就发送出去,简单但是有可能会出现丢包的情况,下面以两端通信为例
注意:这里的步骤跟TCP有很大的差异,具体如下
- 两端都有一个ip地址和端口号,而且两端都需要通过DatagramSocket类创建自己对应ip和端口号的socket类
- 发送数据的时候,需要指定对面的ip和端口号,接收数据的时候,自己的ip和端口号必须跟发送端的一致,这样才能接收到数据
- 关闭资源
- 可以看到,指定端口号与ip的方式跟TCP直接两端建立了一个流管道的方式通信有很大差异,而且udp的方式只要知道对方的ip跟端口号就可以通信,即可以实现一对多通信
客户端Client
public class Client {
public static void main(String[] args) {
try {
//不知道端口号,默认随机分配
DatagramSocket socket = new DatagramSocket();
//封装数据并且发送数据
String str = "hello! 在没";
byte[] bytes = str.getBytes();
//获取要发送的服务端ip信息
InetAddress add = InetAddress.getByName("127.0.0.1");
int port = 8888;
//封装成一个数据包
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, add, port);
//发送数据包
socket.send(datagramPacket);
//关闭资源
socket.close();
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端Server
public class Server {
public static void main(String[] args) {
try {
//DatagramSocket:用于发送或接收数据包
//注意:这里的端口号必须跟client的发送过来的端口号一致,表示只接收这个端口号发过来的信息
DatagramSocket socket = new DatagramSocket(8888);
//创建字节数组,存放数据包数据
byte[] buff = new byte[1024];
//创建一个数据包
DatagramPacket datagramPacket = new DatagramPacket(buff, buff.length);
//将接收的数据放到数据包中
socket.receive(datagramPacket);
//将数据转为字符串输出
String s = new String(datagramPacket.getData(), 0, datagramPacket.getLength());
System.out.println("s = " + s);
//关闭资源
socket.close();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
五、关于TCP与UDP
UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
简述“3次握手”和“4次挥手”
第一次握手
- 客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。
第二次握手
- 服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。
第三次握手
- 当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
四次挥手
第一次握手
- 若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
第二次握手
- B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。
第三次握手
- B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
第四次握手
- A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。