网络编程
首先我们要知道,到目前为止,我们的代码力所能及的地方,都还在自己的电脑上。但现在上网,都是要与其他服务器交互,通信的。
网络编程即使用套接字来达到进程间通信,现在一般称为TCP/IP编程。
TCP/IP协议
传输控制协议/因特网互联协议(Transmission Control Protocol/Internet Protocal),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
TCP/IP协议中的四层: 应用层、传输层、网络层和链路层
- 应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。
- 传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
- 网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
- 数据链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
UDP
- 用户数据报协议(User Datagram Protocol)。
- 数据报(Datagram):网络传输的基本单位
- UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
- 由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
- 但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时,不建议使用UDP协议。
- 特点:数据被限制在64kb以内,超出这个范围就不能发送了。
TCP
- 传输控制协议(Transmission Control Protocol)。
- TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
- 在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。
TC协议——“三次握手”
TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手,客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
(客户端向服务器端发出连接请求,等待服务器确认。) - 第二次握手,服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。(服务器端向客户端回送一个响应,通知客户端收到了连接请求。)
- 第三次握手,客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。(客户端再次向服务器端发送确认信息,确认连接。)
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
TC协议——“四次挥手”
其次,TCP的客户端和服务端断开连接,需要四次挥手
- 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
- 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
- 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
- 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
- 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
- 服务器收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
- 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。
用途中通俗理解就是:
三次握手:
男:我准备出门去找你了
女:ok,我知道了,你可以出门了
男:行,我出门了
四次挥手:
男:我要回家写代码去了
女:准备走了?
女:不再坐会儿?
男:走了走了,代码要紧
部分借鉴自博主:
ZaynFox
原文链接
套接字
创建套接字对象,连接服务器,流使用传输数据
public class Client {
public static void main(String[] args)throws Exception {
//创建一个 连接服务器的 套接字 对象
Socket socket = new Socket();
//连接服务器
socket.connect(new InetSocketAddress("localhost",8080));
try (//获取服务器响应 的数据
InputStream in = socket.getInputStream();
//包装成缓冲流
BufferedReader bis = new BufferedReader(new InputStreamReader(in));
//服务器连接后
// 获取 输出流
OutputStream out = socket.getOutputStream();
//包装成缓冲流
BufferedWriter bos = new BufferedWriter(new OutputStreamWriter(out));
){
//发送信息
bos.write("hello,服务器1");
bos.newLine();
bos.flush();
bos.newLine();
// bos.write("");
bos.flush();
//接收数据
StringBuilder sb = new StringBuilder();
String line = "";
while ((line = bis.readLine()) != null){
sb.append(line);
}
System.out.println(sb);
}finally {
socket.close();
}
}
}
绑定 IP和端口 并设置 监听数量
hostname 使用 本地IP地址 localhost : 代表本机 IP
127.0.0.1 代表本机 IPcnm输入ipconfig:
本机IP :192.168.0.118
port 端口号 是一个数字 不能超过65535:
8080,8000,8888,9999,7000,7001
public class Server {
public static void main(String[] args) throws Exception{
//创建一个 套接字 Socket 对象
ServerSocket ss = new ServerSocket();
//绑定 IP和端口 并设置 监听数量
/**
* hostname 使用 本地IP地址
*
* localhost : 代表本机 IP
* 127.0.0.1 代表本机 IP
*
* cnm输入ipconfig: 本机IP :192.168.0.118
*
* port 端口号
* 是一个数字 不能超过65535
*
* 8080,8000,8888,9999,7000,7001
*
*/
ss.bind(new InetSocketAddress("localhost",8080),5);
// 有很多人连接服务器,所以开启一个死循环,不断接收客户端的连接
while (true) {
// 被动等待 客户端的连接,会产生阻塞现象
//返回一个 socket 用来连接 客户端与服务器
//服务器 向 客户端 发送数据的通道
Socket socket = ss.accept();
new Thread(() ->{
try ( //接收 客户端 发送的数据 获取输入流
InputStream in = socket.getInputStream();
//包装成缓冲流
BufferedReader bis = new BufferedReader(new InputStreamReader(in));
//读取客户端发送的数据
OutputStream out = socket.getOutputStream();
//包装成缓冲流
BufferedWriter bos = new BufferedWriter(new OutputStreamWriter(out));
){
//打印
StringBuilder sb = new StringBuilder();
String line = "";
//读一行数据
while (!(line = bis.readLine()).equals("")){
sb.append(line);
}
System.out.println(sb);
//发个数据
bos.write("我是给服务器发的数据:别卡了,给爷爬");
//记得关闭资源 通道资源也要关
}catch (Exception e){
e.printStackTrace();
}
finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
综合应用
创建一个在线聊天系统
聊天软件其实是两个用户发给服务器,服务器再反送给对方。同时,支持多人聊天,需要用到多线程的操作。
首先创建服务器类
public class ChatClient {
private String qq;
private Socket socket;
/**
* 接受一个 聊天的 客户端身份
*/
public ChatClient(String qq)throws Exception{
this.qq = qq;
//创建一个Socket 对象
socket = new Socket();
socket.connect(new InetSocketAddress("localhost",9999));
//获取输出流,表明身份
OutputStream stream = socket.getOutputStream();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(stream));
out.write(qq);
out.newLine();
out.flush();
}
public void Chat(String to){
//开启一个线程 负责发送信息
new Thread(new WriteMsg(socket,to)).start();
//开启另一个线程 负责读取 服务器发来的 信息
new Thread(new ReadMsg(socket)).start();
}
}
然后编写Server层逻辑
public class ChatServer {
/**
* 存储所有在线用户的 身份和 socket 通道
*/
private static Map<String, Socket> map = new HashMap<>();
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket();
//绑定IP和端口
serverSocket.bind(new InetSocketAddress("localhost",9999));
//开启一个死循环,负责监听 和 可客户端的连接
while (true){
//阻塞式 等待客户端的连接
Socket socket = serverSocket.accept();
//获取 客户端 发送的身份信息 ,并存储到 map 容器中
//开线程
new Thread(() ->{
try{
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//获取 客户端的发送的信息
String s = in.readLine();
map.put(s,socket);
System.out.println(map);
//开启一个while死循环 ,不断接收客户端发来的信息
while (true){
String msg = in.readLine();//处理后的信息
if(msg == null)continue;
//开始处理
String[] split = msg.split(":",2);//拆分
System.out.println(msg);
String id = split[0];//拿到 发送人
msg = split[1];//拿到 需要发送的信息
//根据 发送人去 map中找到对应身份的 socket
//其中 他与某人聊天的通道Socket是value
Socket socket1 = map.get(id);
//判空
if(socket1 == null){
//提示对方不在线
out.write("对方不在线");
out.newLine();
out.flush();
}else {
//不为空 找到对应的Socket
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket1.getOutputStream()));
bw.write(String.format("%s:%s",s,msg));
bw.newLine();
bw.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
然后创建两个类来模拟两个用户:
public class ChatClientTest {
public static void main(String[] args) throws Exception{
//发送人
ChatClient client = new ChatClient("脖子");
//发送给
client.Chat("门子");
}
}
public class ChatClientTest2 {
public static void main(String[] args) throws Exception{
//发送人
ChatClient client = new ChatClient("门子");
//发送给
client.Chat("脖子");
}
}
先启动服务器,然后启动两个用户:
然后就可以发送信息了,还能在服务器查看数据:
发送人:
接收人:
服务器
简单的缓冲流显示数据
必须实现Runnable 接口 重写run()方法
写:
public class WriteMsg implements Runnable{
private Socket socket;
private String to;
public WriteMsg(Socket socket,String to){
this.socket = socket;
this.to = to;
}
@Override
public void run() {
//获取从控制台输入的信息
Scanner scanner = new Scanner(System.in);
while (true){
String text = scanner.nextLine();
if("exit".equals(text))break;
//将信息发送给 服务器
String msg = String.format("%s:%s",to,text);
//将信息发送给服务器
try {
//获取输出流,表明身份
OutputStream outputStream = socket.getOutputStream();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(outputStream));
out.write(msg);
out.newLine();
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
读:
public class ReadMsg implements Runnable{
private Socket socket;
public ReadMsg(Socket socket){
this.socket = socket;
}
@Override
public void run() {
while (true){
try{
//创建缓冲流
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//读数据、并显示在控制台上 即可
String s = in.readLine();
if(s == null)continue;
System.out.println(s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}