一、网络编程的一些基本概念 (一)网络通信的三个基本要素 1、通信协议: ? ? 在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。 ? ? 理论上有7层,OSI参考模型。物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。 ? ? 这个理论太过于精细,过了理论化,实际中没有真正实施,而是有另一个模型代替。 ? ? 实际发挥作用的是TCP/IP协议参考模型。 ? ? 这个模型是分为四层:物理层+数据链路层、网络层、传输层、应用层。 ? ? TCP/IP协议参考模型的协议有很多,为什么用TCP和IP两个协议作为命名呢? ? ? 因为TCP/IP协议是在最早定下来的(大家最早达成一致协议),并且这两个协议是最最重要的。
TCP:传输控制协议 (Transmission Control Protocol)。面向连接的,可靠的,基于字节流的网络协议。 ? ? ?面向连接:如果通信双方要通信之前,必须是联通状态的。 ? ? ? ? ? ? 在传输数据之前,会进行“三次握手”来确保网络连接是正常的。 ? ? ? ? ? ? TCP协议的程序是分为客户端(包括B和C)和服务器端。 ? ? ? ? ? ? 第一次握手:客户端先发起请求,会给服务器发一些状态码 ? ? ? ? ? ? ? ? ? ? 比喻:问服务器,我能和你连接吗? ?暗号:今晚约吗? ? ? ? ? ? ? 第二次握手:服务器响应客户端,会给客户端返回状态码,并且加一些状态码信息 ? ? ? ? ? ? ? ? ? ? 比喻:可以连接? ? ?回复:你刚才问的是“今晚约吗?”,我回答“可以约” ? ? ? ? ? ? ? ? ? ? 服务器回复你刚才问的是“今晚约吗?”的意义,是让客户端确认它刚才给服务器的消息是正确传递的。 ? ? ? ? ? ? 第三次握手:客户端再次回应服务器。 ? ? ? 回复:你刚才说的是“可以约”,我回答“老地方见” ? ? ? ? ? ? ? ? ? ? 客户端回复你刚才说的是“可以约”,让服务器确认它发给客户端的消息是正确传递的
? ? ? ? ? ? 三次握手没问题之后,再发送真实的数据。
? ? ? ? ? ? 在传输数据结束,会进行“四次挥手”来断开连接。 ? ? ? ? ? ? 第一次挥手:客户端先发起断开连接的请求。 ? ? ? ? ? ? ? ? ? ? 服务器收到后,要关闭接收数据通道,表示服务器不再接收客户端发送的数据了。 ? ? ? ? ? ? 第二次挥手:服务器响应客户端,告诉客户端,它知道了要断开了, ? ? ? ? ? ? ? ? ? ? 但是要通知客户端,你别着急彻底断开,我给你的数据还没完,你要把剩下的数据接收完,再断开。 ? ? ? ? ? ? 第三次挥手:服务器把剩下的数据发送完了之后,告诉客户端,现在咱们可以彻底断开了。 ? ? ? ? ? ? 第四次挥手:客户端会告诉服务器,我准备好了,然后可以彻底断开了。 ? ? ? ? ? ? ? ? ? ? 第四次挥手时,客户端在发送完最后的“拜拜”时,会等一会儿。 ? ? ? ? ? ? ? ? ? ? 等待的意义是让服务器确实收到了最后的断开的消息。 ? ? ? ? ? ? ? ? ? ? 如果对方没收到,TCP协议会让客户端重传,这个时候,客户端还发送一遍,再次等待。 ? ? 可靠的:如果中间丢包了,TCP会让对方重传。 UDP:用户数据报协议(User Datagram Protocol)非面向连接的,不可靠的,基于数据报的协议。 ? ? 非面向连接的:发送方不管接收方是否在线,都是直接发送消息。 ? ? 不可靠:如果丢包了,就丢了。 ? ? 优点:快 ? ? 适用于:实时会议,视频等
网络程序的结构:B/S,C/S结构 B:浏览器browser,它本质上也是客户端,只是统一的客户端 S:服务器端server C:客户端 ? 需要用户单独下载和安装的客户端程序
2、IP地址:消息(数据)从哪里来到哪里去,都需要通过IP地址标识 ? ? IP地址是用来唯一的定位/标识一台主机用的。
? ? 域名:为了方便用户记忆主机地址。 ? ? ? ? www.atguigu.com ?<= ?域名解析器 ?=>IP地址
? ? 本地回环IP:127.0.0.1
3、端口号 ? ? 同一台主机上同时有多个应用程序都在和网络进行通信, ? ? 那么端口号就是用来唯一定位/标识一个应用程序的。
? ? mysql:3306 ? ? tomcat:8080 ? ? http:80 ? ? 。。。。
? ? 公共端口:0-1023 ?1024个 ? ? 注册端口:被一些知名软件或应用程序注册 ? ? 动态/ 私有端口:49152~65535
? (二)Socket编程 无论基于TCP协议还是基于UDP协议的网络通信程序,都要用到Socket进行编程。 Socket(套接字):代表网络通信的一个端点。负责和网卡驱动程序交换,发送或接收消息。涉及到IP地址、端口号等内容。 分为两大类: (1)流套接字:用于TCP协议编程 ? ? ? ? SeverSocket:负责客户端和服务器端的连接用,即是服务器端用来等待和接收客户端连接用的,不负责传输数据 ? ? ? ? Socket:客户端和服务器端传输数据用的 (2)数据报套接字:用于UDP协议编程 ? ? ? ? DatagramSocket:既要负责连接,又要负责传输数据。
端口号:int类型的数字,范围0-65535之间 IP地址: ? ? (1)字符串:“127.0.0.1” ? ? ? ? 不是所有的API都支持字符串类型 ? ? (2)byte[]: ? ? ? ? {127,0,0,1} ? ? ? ? 不是所有的API都支持byte[]类型 ? ? ? (3)InetAddress类的对象
public class TestIPAndPort {
public static void main(String[] args) throws Exception{
//获取本地IP地址
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);//LAPTOP-AG8KORH9/192.168.131.1
//LAPTOP-AG8KORH9/192.168.35.82
//获取尚硅谷官网的ip地址
InetAddress atguigu = InetAddress.getByName("www.atguigu.com");
System.out.println(atguigu);
byte[] ips = {(byte)192,(byte)168,35,82};
InetAddress address = InetAddress.getByAddress(ips);
/*
192的二进制 00000000 00000000 00000000 11000000(int)
11000000(byte)
底层源码中是使用int处理的,会在11000000前面加24个0恢复原来
ip地址的每一个数字是[0,255] [00000000,11111111 ]
*/
}
}
二、UDP:用户数据报协议(User Datagram Protocol)
接收方:(先运行才能收到消息) 第一步:准备一个Socket ? ? DatagramSocket ds = new DatagramSocket(8888); ? ? IP地址:默认是本机 第二步:准备数据报 ? ? byte[] data = new byte[1024]; ? ? DatagramPacket dp = new DatagramPacket(data, data.length); 第三步:接收数据 ? ? ? ds.receive(dp); 第四步:处理数据 ? ? 数据在字节数组中,长度是dp.getLength() 第五步:关闭 ? ? ds.close();
public class TestReceive {
public static void main(String[] args)throws Exception {
// 第一步:准备一个Socket
DatagramSocket ds = new DatagramSocket(8888);
// 第二步:准备数据报
//DatagramPacket(byte[] buf, int length)
byte[] data = new byte[1024];
DatagramPacket dp = new DatagramPacket(data, data.length);
//第三步:接收数据
//通过socket,接收数据报
ds.receive(dp);
//第四步:处理数据
int length = dp.getLength();//实际接收的数据的长度
String str = new String(data, 0, length);
System.out.println(str);
// 第五步:关闭
ds.close();
}
}
发送方: 步骤: 第一步:准备一个Socket ? ? DatagramSocket ds = new DatagramSocket(); ? ? 这边的端口号:随机分配 ? ? IP地址:默认是本机 第二步:准备数据报 DatagramPacket(byte[] buf, int length) ? ? ? ? ? 构造 DatagramPacket,用来接收长度为 length 的数据包。 DatagramPacket(byte[] buf, int length, InetAddress address, int port) ? ? ? ? ? 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。 DatagramPacket(byte[] buf, int offset, int length) ? ? ? ? ? 构造 DatagramPacket,用来接收长度为 length 的包,在缓冲区中指定了偏移量。 DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) ? ? ? ? ? 构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。 DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) ? ? ? ? ? 构造数据报包,用来将长度为 length 偏移量为 offset 的包发送到指定主机上的指定端口号。 DatagramPacket(byte[] buf, int length, SocketAddress address) ? ? ? ? ? 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
第三步:发生数据 ? ? ds.send(dp); 第四步:关闭 ? ? ds.close();
public class TestSend {
public static void main(String[] args)throws Exception {
//第一步:准备一个Socket
DatagramSocket ds = new DatagramSocket();
//第二步:准备数据报
//DatagramPacket(byte[] buf, int length, InetAddress address, int port)
String data = "明天终于可以休息了";
byte[] buf = data.getBytes();//默认使用平台的编码方式UTF-8进行编码
//发给自己
// InetAddress address = InetAddress.getLocalHost();//接收方现在也是本机
/*
发给同桌:
*/
byte[] ips = {(byte)192,(byte)168,35,82};
InetAddress address = InetAddress.getByAddress(ips);
/*
192的二进制 00000000 00000000 00000000 11000000(int)
11000000(byte)
底层源码中是使用int处理的,会在11000000前面加24个0恢复原来
ip地址的每一个数字是[0,255] [00000000,11111111 ]
*/
DatagramPacket dp = new DatagramPacket(buf,buf.length, address, 8888);//8888接收方的端口号
//第三步:发生数据
//通过socket发生数据报
ds.send(dp);
System.out.println("发送完毕");
// 第四步:关闭
ds.close();
}
}
三、TCP:传输控制协议 (Transmission Control Protocol)
服务器端: 第一步:开启服务器,注册监听的端口号 ? ? ServerSocket server = new ServerSocket(8888); 第二步:监听并接收客户端的连接 ?,如果成功了,就会有一个Socket对象
第三步:可以接收也可以发送 ? ? 假设我这里是先演示接收一个消息
第四步:关闭
public class Server1{
public static void main(String[] args)throws Exception {
// 第一步:开启服务器,注册监听的端口号
ServerSocket server = new ServerSocket(8888);
//第二步:监听并接收客户端的连接
Socket socket = server.accept();
System.out.println("一个客户端连接成功了");
System.out.println("这个客户端的IP地址:" + socket.getInetAddress().getHostAddress());
/* 第三步:可以接收也可以发送
假设我这里是先演示接收一个消息*/
//接收消息需要输入流
InputStream inputStream = socket.getInputStream();
byte[] data = new byte[1024];
int len;
while((len = inputStream.read(data)) != -1){
System.out.println(new String(data,0,len));
}
//第四步:关闭
inputStream.close();
socket.close();
server.close();
}
}
public class Server2{
public static void main(String[] args)throws Exception {
// 第一步:开启服务器,注册监听的端口号
ServerSocket server = new ServerSocket(8888);
//第二步:监听并接收客户端的连接
Socket socket = server.accept();
System.out.println("一个客户端连接成功了");
System.out.println("这个客户端的IP地址:" + socket.getInetAddress().getHostAddress());
/* 第三步:可以接收也可以发送*/
//(1)接收消息需要输入流
InputStream inputStream = socket.getInputStream();
byte[] data = new byte[1024];
int len;
while((len = inputStream.read(data)) != -1){
System.out.println(new String(data,0,len));
}
//(2)返回消息,使用输出流
OutputStream outputStream = socket.getOutputStream();
String str = "hi";
outputStream.write(str.getBytes());
//第四步:关闭
outputStream.close();
inputStream.close();
socket.close();
server.close();
}
}
/*
TCP编程的服务器端:
可以同时接收多个客户端的连接
可以接收多次的客户端的消息,每次接收完客户端发送的消息后,反转消息内容返回
*/
public class Server3 {
public static void main(String[] args)throws Exception {
//第一步:开启服务器,并且注册端口号
ServerSocket server = new ServerSocket(8888);
int count = 0;
while(true) {
//第二步:接收客户端的连接
Socket socket = server.accept();
System.out.println("第" + ++count + "个客户端连接,它的IP是:" + socket.getInetAddress());
//每接收一个客户端,就分配一个线程给他单独通信
MessageThread thread = new MessageThread(socket);
thread.start();
}
// server.close();//服务器不关了
}
}
class MessageThread extends Thread{
private Socket socket;
public MessageThread(Socket socket) {
this.socket = socket;
}
public void run(){
try(
//第三步:接收客户端发送的词语
//因为本例中使用的是纯文本消息,所以可以使用按行读的方式
InputStream inputStream = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr);
OutputStream outputStream = socket.getOutputStream();
//为了对方能够按行读,我们这里按行写
PrintStream ps = new PrintStream(outputStream);
) {
String line;
while ((line = br.readLine()) != null) {
//第四步:返回给客户端
StringBuilder s = new StringBuilder(line);
ps.println(s.reverse());
}
}catch(IOException e){
e.printStackTrace();
}finally{
//第五步:端开连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*
TCP编程服务器端:
同时接收多个客户端上传文件,上传后的文件保存在服务器的upload文件夹下。
每次接收完一个客户端的文件之后,返回一句话“接收xx文件完毕”
*/
public class Server4 {
public static void main(String[] args)throws Exception {
//第一步:开启服务器
ServerSocket server = new ServerSocket(8888);
//第二步:接收客户端连接
while(true){
Socket socket = server.accept();
//每一个客户端分配一个线程
FileUploadThread thread = new FileUploadThread(socket);
thread.start();
}
}
}
class FileUploadThread extends Thread{
private Socket socket;
public FileUploadThread(Socket socket) {
this.socket = socket;
}
public void run(){
FileOutputStream fos = null;
//第三步:准备接收文件的名字(含后缀名),文件内容
//每次接收完一个客户端的文件之后,返回一句话“接收xx文件完毕”
try(
InputStream inputStream = socket.getInputStream();//负责从网络(客户端)接收数据
DataInputStream dis = new DataInputStream(inputStream);
/*
使用DataInputStream的目的,是因为这个IO流有readUTF()读字符串的方法,
又有read(byte[] data)读字节内容方法
*/
OutputStream outputStream = socket.getOutputStream();//负责返回给客户端结果
PrintStream ps = new PrintStream(outputStream);
//PrintStream也是一种输出流,因为它有println方法,可以在输出xxx内容之后换行,即可以实现按行输出
//至于它的内容输出到哪里,要看它的构造器如何指定
//PrintStream ps = new PrintStream(outputStream); ps.println(xx)的数据xx就输出到outputStream流
//PrintStream ps2 = new PrintStream(new File("文件名")); ps.println(xx)的数据xx就输出到文件中
){
//(1)先接收文件名
String fileName = dis.readUTF();
//(2)避免文件的重名问题
//可以使用A:随机数,B:时间戳,C:IP地址加时间戳,D:UUID等工具类生成唯一的一串数字 E:时间戳+原来的文件名
//这里演示时间戳的方式
long time = System.currentTimeMillis();
String newFileName = time+fileName;
//(3)准备输出文件内容的FileOutputStream流
fos = new FileOutputStream("upload/" + newFileName);//负责把接收的文件内容,存储到服务的upload文件夹的某个文件中
//(4)从网络中接收文件内容,并输出到服务的newFileName文件中
byte[] data = new byte[1024];
int len;
while((len = dis.read(data)) != -1){
fos.write(data,0,len);
}
//(5)返回结果
ps.println(fileName + "文件接收完毕!");
}catch (IOException e){
e.printStackTrace();
}finally {
try {
if(fos!=null){
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//实现群聊,提醒上线、下线、掉线
public class Server5 {
private static ArrayList<Socket> online = new ArrayList<>();
public static void main(String[] args)throws Exception {
//第一步:开启服务器
ServerSocket server = new ServerSocket(8888);
//第二步:接收客户端连接
while(true){
Socket socket = server.accept();
//每连接一个客户端,需要把它添加online集合中
online.add(socket);
//每一个客户端分配一个线程
MessageHandler thread = new MessageHandler(socket);
thread.start();
}
}
static class MessageHandler extends Thread{
private Socket socket;//接收消息
private String ip;
public MessageHandler(Socket socket) {
this.socket = socket;
this.ip = socket.getInetAddress().getHostAddress();
}
public void run(){
sendMessageToOthers(ip + "上线了");
try(
InputStream inputStream = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr);
){
String line;
while((line = br.readLine()) != null){
if("bye".equalsIgnoreCase(line)){
PrintStream printStream = new PrintStream(socket.getOutputStream());
printStream.println("bye");//给自己返回bye,用于接收客户端的接收线程
}else {
sendMessageToOthers(ip + "说:" + line);
}
}
}catch (IOException e){
e.printStackTrace();
sendMessageToOthers(ip + "掉线了");
//从online移除
online.remove(socket);
}finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
sendMessageToOthers(ip + "下线了");
//从online移除
online.remove(socket);
}
}
public void sendMessageToOthers(String content){
//遍历online集合,可以拿的所有客户端的socket,可以给它们返回消息
Iterator<Socket> iterator = online.iterator();
while (iterator.hasNext()){
Socket s = iterator.next();
try {
if(!s.equals(socket)){//不要给自己发
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println(content);
//这里不能关闭ps和s,因为这个客户端还要用呢
}
} catch (IOException e) {
e.printStackTrace();
//s客户端发送消息失败,说明s客户端已经“掉线”等异常,
//应该从online中把它删除
iterator.remove();
}
}
}
}
}
客户端: 如果服务器端没有开,或者你写的IP地址和端口号错,会遇到 ? ? java.net.ConnectException: Connection refused: connect ?拒绝连接,连接失败
第一步:主动连接服务器,需要指定服务器端的IP地址和端口号
第二步:可以接收也可以发送 ? ? 这里演示发送一个消息 第三步:关闭
public class Client1 {
public static void main(String[] args) throws Exception{
// 第一步:主动连接服务器,需要指定服务器端的IP地址和端口号
Socket socket = new Socket("127.0.0.1",8888);
// 第二步:可以接收也可以发送
//发送消息需要输出流
OutputStream outputStream = socket.getOutputStream();
String str = "hello";
outputStream.write(str.getBytes());
// 第三步:关闭
outputStream.close();
socket.close();
}
}
public class Client2 {
public static void main(String[] args) throws Exception{
//第一步:主动连接服务器,需要指定服务器端的IP地址和端口号
Socket socket = new Socket("127.0.0.1",8888);
// 第二步:可以接收也可以发送
//(1)发送消息需要输出流
OutputStream outputStream = socket.getOutputStream();
String str = "hello";
outputStream.write(str.getBytes());
// outputStream.close();//错误的
socket.shutdownOutput();//半关闭,关闭输出通道,但是不关闭输入通道
//(2)接收返回的消息
System.out.println("服务器返回的消息是:");
InputStream inputStream = socket.getInputStream();
byte[] data = new byte[1024];
int len;
while((len = inputStream.read(data)) != -1){
System.out.println(new String(data,0,len));
}
// 第三步:关闭
outputStream.close();
inputStream.close();
socket.close();
}
}
TCP编程的客户端:
可以从键盘输入单词/成语等,给服务器发过去,并接收服务器反转后的内容。
输入一个发送一个,直到从键盘输入stop为止。
*/
public class Client3 {
public static void main(String[] args) throws Exception{
//第一步:连接服务器,需要指定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1",8888);
OutputStream outputStream = socket.getOutputStream();
//为了对方能够按行读,我们这里按行写
PrintStream ps = new PrintStream(outputStream);
//因为本例中使用的是纯文本消息,所以可以使用按行读的方式
InputStream inputStream = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr);
//第二步:从键盘输入单词、成语等,直到输入stop为止
Scanner input = new Scanner(System.in);
while(true){
System.out.print("输入单词或成语等:");
String word = input.next();
if("stop".equalsIgnoreCase(word)){
break;
}
//第三步:给服务器发送过程
ps.println(word);
//第四步:接收服务器返回的消息
System.out.println("服务器返回的消息是:" + br.readLine()) ;
}
//第五步关闭
ps.close();
outputStream.close();
br.close();
isr.close();
inputStream.close();
socket.close();
}
}
/*
TCP编程客户端:
从键盘输入要上传的文件的具体路径,上传文件,并接收服务器反馈的消息
*/
public class Client4 {
public static void main(String[] args)throws Exception {
//第一步:连接服务器
Socket socket = new Socket("127.0.0.1", 8888);
//第二步,从键盘输入要上传的文件的具体路径
Scanner input = new Scanner(System.in);
System.out.print("请输入要上传的文件的路径:");
String filePath = input.nextLine();
//D:\Download\img\美女\15.jpg
File file = new File(filePath);
//第三步:给服务器上传文件名,文件内容
OutputStream outputStream = socket.getOutputStream();
//为了区别文件名和文件内容,使用DataOutputStream
DataOutputStream dos = new DataOutputStream(outputStream);
//输出文件名
dos.writeUTF(file.getName());
//输出文件内容
FileInputStream fis = new FileInputStream(filePath);
byte[] data = new byte[1024];
int len;
while((len = fis.read(data)) != -1){
dos.write(data,0,len);
}
//半关闭
socket.shutdownOutput();
//第四步:接收服务器反馈的消息
InputStream inputStream = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr);
System.out.println(br.readLine());
//第五步关闭
br.close();
isr.close();
inputStream.close();
fis.close();
dos.close();
outputStream.close();
input.close();
socket.close();
}
}
//实现群聊,提醒上线、下线、掉线
public class Client5 {
public static void main(String[] args) throws Exception{
//第一步:连接服务器
Socket socket = new Socket("127.0.0.1", 8888);
SendThread send = new SendThread(socket);
send.start();
ReceiveThread receiveThread = new ReceiveThread(socket);
receiveThread.start();
send.join();
receiveThread.setFlag(false);
receiveThread.join();
socket.close();//socket关闭时,所有的IO流自动关闭了
}
}
class SendThread extends Thread{
private Socket socket;
public SendThread(Socket socket) {
this.socket = socket;
}
public void run(){
Scanner input = null;
PrintStream ps = null;
try {
input = new Scanner(System.in);
ps = new PrintStream(socket.getOutputStream());
while(true){
System.out.print("请输入要发送的消息内容:");
String message = input.next();
ps.println(message);
if("bye".equalsIgnoreCase(message)){
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}finally{
// ps.close();//在这里不能关闭IO流
input.close();
}
}
}
class ReceiveThread extends Thread{
private Socket socket;
private boolean flag = true;
public ReceiveThread(Socket socket) {
this.socket = socket;
}
public void run(){
try(
InputStream inputStream = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr);
){
while(flag){
String line =br.readLine();
System.out.println("接收的消息:" + line);
}
}catch(IOException e){
e.printStackTrace();
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
|