java中的网络编程(一)
网络编程
java后端中的网络编程, 很多时候并不需要自己编写底层代码。但是对于网络底层的原理,必须要足够了解。才能理解和学习后续更高阶的技术, 比如负载均衡。
网络编程的基础知识
通信协议分层模型
通信协议分层模型
通信协议分层模型
TCP 协议
TCP(Transmission Control Protocol,传输控制协议):是一种面向连接的
、可靠的
、基于字节流
的传输层的通信协议。
• 使用TCP协议前,须先建立TCP连接,形成传输数据通道。传输前,采用“三次握手”方式,是可靠的。
• TCP协议进行通信的两个应用进程:客户端、服务端
• 在连接中可进行大数据量的传输
• 传输完毕,需释放已建立的连接,效率低
• 类似于电话
UDP 协议
UDP(User Datagram Protocol,用户数据报协议):是一个无连接
的传输层协议、提供面向事务
的简单不可靠的信息传送服务。
• 将数据、源、目的封装成数据包,不需要建立连接
• 每个数据报的大小限制在64K内
• 因无需连接,故是不可靠的,数据可能丢失
• 发送数据结束时无需释放资源,速度快
• 类似于短信
IP协议
IP 是无连接的通信协议。它不会占用两个正在通信的计算机之间的通信线路。
(这样,IP 就降低了对网络线路的需求。每条线可以同时满足许多不同的计算机之间的通信需要)通过 IP,消息(或者其他数据)被分割为小的独立的包,并通过因特网在计算机之间传送
。IP 负责将每个包路由至它的目的地
。当一个 IP 包从一台计算机被发送,它会到达一个 IP 路由器。IP 路由器负责将这个包路由至它的目的地,直接地或者通过其他的路由器。在一个相同的通信中,一个包所经由的路径可能会和其他的包不同。而路由器负责根据通信量、网络中的错误或者其他参数来进行正确地寻址。TCP/IP 意味着 TCP 和 IP 在一起协同工作
。
- TCP 负责
应用软件(比如你的浏览器)和网络软件
之间的通信。
- IP 负责
计算机之间
的通信。
- TCP 负责将
数据分割
并装入 IP 包,然后在它们到达的时候重新组合它们。
- IP 负责将包发送至接受者。
TCP连接的三次握手和四次挥手
- TCP是面向连接的, 说的是TCP的连接的建立是需要通过三次握手的认证后才建立.而TCP连接一旦要端开, 还需要进行四次挥手.
我们需要想下TCP连接的建立, 为什么要经过三次握手?而不是两次?这三次握手是哪三次?而断开连接为什么要经过四次握手?这四次又是哪四次?具体底层是以什么体现?
三次握手
说到三次握手, 这里引用网络上的关于三次握手原因的类比.
第一次对话
老婆让甲出去打酱油,半路碰到一个朋友乙,甲问了一句:哥们你吃饭了么?
结果乙带着耳机听歌呢,根本没听到,没反应。甲心里想:跟你说话也没个音,不跟你说了,沟通失败。说明乙接受不到甲传过来的信息的情况下沟通肯定是失败的。
如果乙听到了甲说的话,那么第一次对话成功,接下来进行第二次对话。
第二次对话
乙听到了甲说的话,但是他是老外,中文不好,不知道甲说的啥意思也不知道怎样回答,于是随便回答了一句学过的中文 :我去厕所了。甲一听立刻笑喷了,“去厕所吃饭”?道不同不相为谋,离你远点吧,沟通失败。说明乙无法做出正确应答的情况下沟通失败。
如果乙听到了甲的话,做出了正确的应答,并且还进行了反问:我吃饭了,你呢?那么第二次握手成功。
通过前两次对话证明了乙能够听懂甲说的话,并且能做出正确的应答。 接下来进行第三次对话。
第三次对话
甲刚和乙打了个招呼,突然老婆喊他,“你个死鬼,打个酱油咋这么半天,看我回家咋收拾你”,甲是个妻管严,听完吓得二话不说就跑回家了,把乙自己晾那了。乙心想:这什么人啊,得,我也回家吧,沟通失败。说明甲无法做出应答的情况下沟通失败。
如果甲也做出了正确的应答:我也吃了。那么第三次对话成功,两人已经建立起了顺畅的沟通渠道,接下来开始持续的聊天。
通过第二次和第三次的对话证明了甲能够听懂乙说的话,并且能做出正确的应答。
可见,两个人进行有效的语言沟通,这三次对话的过程是必须的。
为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。
这个例子举得挺好的。不过个人感觉为什么是三次而不是二次,不是因为为了证明甲能听懂乙并回应(第二次乙能正确的响应甲说明俩人之间沟通已无障碍了),而是怕出现以下情况而浪费感情。这个情景是这样的(例子有点不实际意会就好):甲在路上跟乙打招呼,由于刮风什么的这句活被吹跑了,然后甲又跟打了个招呼,乙听到了并作出了回应。此时不管是三次握手还是两次握手两个人都能愉快的沟通。0.1秒后俩人四次挥手告别了。此时被风刮跑的那句话又传到了乙的耳朵里,乙认为甲又要跟他沟通,所以做出了响应的回应。(问题出现了)假如采用2次握手,乙就认定了甲要跟他沟通,于是就不停的等,浪费感情。可如果是采用3次握手,乙等了一会后发现甲没有回应他就认为甲走了然后自己也就走了!
这就很明白了,其实第三步是防止了乙的一直等待而浪费自己的时间,而不是为了保证甲能够正确回应乙的信息
三次握手总结
三次握手原因
- 为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手
- 为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手.
- 第三步是防止了乙的一直等待而浪费自己的时间,而不是为了保证甲能够正确回应乙的信息**
三次握手协议细节

- 第一次握手:建立连接。
客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
- 第二次握手:服务器收到SYN报文段。
服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK报文段。
然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
完成了三次握手,客户端和服务器端就可以开始传送数据。
四次挥手(分手)
当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了神秘的“四次分手”。
- 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
- 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
- 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
- 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
至此,TCP的四次分手就这么愉快的完成了。当你看到这里,你的脑子里会有很多的疑问,很多的不懂,感觉很凌乱;没事,我们继续总结。
要理解三次握手和四次分手的具体协议细节, 还需要了解很多TCP协议细节.不是一篇博客能讲完的.所以如果看不懂.可以先类比着图和生活中的例子记下.后续有时间我再整理下关于传输层通信协议的一些细节。
java中的网络编程
InetAddress
InetAddress封装了IP地址和主机信息.
不包括端口
创建IP对象
getByAddress(String host, byte[] addr)
创建IP对象, 不是通过new。而是通过getByAddress(String host, byte[] addr)
其中host表示主机名, 而addr是ip地址.
注意:
-
byte[] addr = {(byte)192, (byte)168, 34, 16}
表示192.168.34.16 - 获取本机IP对象可以用
getLocalAddress()
java中基于Socket的UDP和TCP通信
UDP通信
java中的UDP通信使用的是DatagramSocket对象和DatagramPacket.
UDP通信 客户端步骤
- 创建DatagramSocket 套接字对象ds
- 创建DatagramPacket 数据包对象dp, 指定
字节数组
,字节数组大小
,目的ip
,目的端口号
- 调用ds.send(dp)方法发送数据包对象
- 释放socket
socket.close()
// 客户端
public class TestDatagram {
public static void main(String[] args) {
// 创建Socket
try {
DatagramSocket socket = new DatagramSocket(); // 默认绑定到本机IP, 并且随机选择端口号.
byte[] data = "hello".getBytes();
byte[] address = {(byte)127,(byte)0, 0, 1};
InetAddress ip = InetAddress.getByAddress(address);
DatagramPacket datagramPacket = new DatagramPacket(data, data.length, ip, 9999);
socket.send(datagramPacket);
// 释放资源
socket.close();
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
UDP通信 服务端步骤
- 创建DatagramSocket 套接字对象ds
- 创建DatagramPacket 数据包对象dp. 指定
字节数组
,字节数组大小
- 调用ds.recevice(dp)
- 关闭ds
UDP通信服务端客户端流程基本一样
// 服务端
public class TestDatagramServer {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(9999, InetAddress.getByAddress(new byte[]{127,0,0,1}));
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
socket.close();
System.out.println(new String(buffer, 0, buffer.length));
}
}
TCP通信
TCP客户端通信步骤
- 根据服务端ip和端口 够照Socket类对象.
- 打开连接到socket对象的输入、输出流.(getInputStream、getOutputStream)
- 按照一定的协议对Socket进行读写
- 关闭socket
// Client端
public class TestSocket {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream outputStream = socket.getOutputStream();
byte[] buffer = "Hello world".getBytes();
outputStream.write(buffer, 0, buffer.length);
outputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCP服务端通信步骤
- 创建服务端套接字, 并监听端口.
调用ServerSocket(int port)
- 等待客户端连接,客户端一连接就是获取和客户端通信socket对象
ss.accept()
- 打开连接到socket对象的输入、输出流.(getInputStream、getOutputStream)
- 关闭Socket和ServerSocket对象
// server端
public class TestSocketServer {
public static void main(String[] args) throws IOException {
// 用于监听的服务端socket
ServerSocket ss = new ServerSocket(9999);
// 用于和客户端建立链接, 进行通信的socket
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = is.read(buffer)) != -1) {
String line = new String(buffer, 0, len);
System.out.println(line);
}
is.close();
socket.close();
ss.close();
}
}
实现一个聊天室
目的
使用java的网络编程API实现一个聊天室.主要为了掌握
- 输入流输出流和处理流的应用
- 集合框架的应用
- java网络通信的基本应用
- 多线程的应用
需求
- Server: 服务器端作为消息转发中心.
1.将某客户端的消息,转发给其他客户端
2.删除服务器中下线, 掉线的客户端.释放其占用的套接字资源
- Client:client端需要实现两个功能。
1.消息的发送
2.消息的接受
并且发送接受是同时进行的
客户端
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("127.0.0.1", 10000);
Thread sender = new Thread(new Sender(socket));
Thread recevie =new Thread(new Receiver(socket));
sender.start();
recevie.start();
sender.join();
recevie.join();
socket.close();
}
}
class Sender implements Runnable {
private Socket socket;
public Sender(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
OutputStream os = null;
Scanner input = null;
PrintStream ps = null;
try {
os = socket.getOutputStream();
input= new Scanner(System.in);
ps = new PrintStream(os);
for (;;) {
System.out.print("请输入要发送的消息:");
String message = input.nextLine();
if ("stop".equals(message)) break;
ps.println(message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
ps.close();
input.close();
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Receiver implements Runnable {
private Socket socket;
public Receiver(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
InputStream is = null;
BufferedReader br = null;
try {
is = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(is));
for (;;) {
String message = br.readLine();
if (null == message) break;
System.out.println("接受到" + socket.getInetAddress().getHostAddress() + "消息: " + message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端
public class ChatServer {
public static void main(String[] args) {
System.out.println("等待连接");
ChatServer chatServer = new ChatServer();
chatServer.start();
}
private ServerSocket serverSocket = null;
private static final int LISTEN_PORT = 10000;
private ArrayList<Socket> onlines = new ArrayList<>();
synchronized public void addClient(Socket socket) {
onlines.add(socket);
}
synchronized public void removeClient(Socket socket) {
onlines.remove(socket);
}
public void start() {
try {
serverSocket = new ServerSocket(LISTEN_PORT);
while (true) {
// accept是同步阻塞.所谓同步是该方法必须执行结束后才能调用其他的方法, 所谓阻塞意味着
// accept方法调用的时候是进行IO操作, 此时CPU是空闲的,
Socket socket = serverSocket.accept();
addClient(socket);
Thread chatThread = new Thread(new ChatThread(socket, this));
chatThread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
synchronized private void sentMsgToOthers(String s, Socket me) {
Iterator iterator = onlines.iterator();
while (iterator.hasNext()) {
Socket client = (Socket)iterator.next();
if (me != client) {
OutputStream os = null;
try {
os = client.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println(s);
} catch (IOException e) {
iterator.remove();
}
}
}
}
private class ChatThread implements Runnable {
private Socket socket;
private ChatServer chatRoom;
public ChatThread(Socket serverSocket, ChatServer chatRoom) {
this.socket = serverSocket;
this.chatRoom = chatRoom;
}
public ChatThread() {
}
@Override
public void run() {
InputStream is = null;
BufferedReader br = null;
try {
System.out.println("客户端" + socket.getInetAddress().getHostAddress() + "连接入服务器");
is = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(is));
String message = null;
while ((message = br.readLine()) != null) {
// 1.先判断消息是不是特殊指令. 下线、断线
if ("stop".equals(message)) {
// 1.1 转发消息给其他客户端节点, 该客户端下线
sentMsgToOthers(socket.getInetAddress().getHostAddress() + "下线了");
break;
}else {
// 2.如果不是特殊指令, 转发消息给其他客户端节点.
sentMsgToOthers(message);
}
}
} catch (IOException e) {
// 转发消息给其他客户端.说明该客户端掉线.
} finally {
try {
if (null != socket)
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void sentMsgToOthers(String s) {
chatRoom.sentMsgToOthers(s, socket);
}
}
}
后续
留点小期待, 后面分享基于NIO的网络编程模型