现在用客户端往服务器端上传一张图片:
//想要从客户端上传给服务端一张图片,
//底层的数据时二进制数,所以读取写入都用字节流对象
import java.io.*;
import java.net.*;
class PicClient {
public static void main(String[] args) throws Exception
{
Socket s = new Socket("192.168.0.104",10006);
//获取流资源
FileInputStream fis = new FileInputStream("11.jpg");
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
//开始上传数据
byte[] buf = new byte[1024];
int len = 0;
while((len = fis.read(buf))!=-1)
{
out.write(buf,0,len);
}
//给一个标记,就说我读完了
s.shutdownOutput();
//开始读取服务器传过来的数据
byte[] by = new byte[1024];
int l = in.read(by);
System.out.println(new String(by,0,l));
fis.close();
s.close();
}
}
class PicServer
{
public static void main(String[] args) throws Exception
{
//获取对象
ServerSocket ss = new ServerSocket(10006);
//打印是那个IP连上了
Socket s = ss.accept();
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"......connected");
FileOutputStream fos = new FileOutputStream("22.jpg");
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
//开始读取并写入数据
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf))!=-1)
{
fos.write(buf,0,len);
}
//给客户端的反馈信息
out.write("上传成功".getBytes());
ss.close();
s.close();
fos.close();
}
}
运行结果:
这个服务端有个局限性,当A客户端连上以后,被服务端获取到,服务端执行具体流程
这时B客户端连接,只有等待
因为服务端还没有处理完A客户的请求,还没有循环回来执行下一次accept方法,所以暂时获取不到B客户端对象
那么为了可以让多个客户端同时并发访问服务端,
服务端最好就是将每个客户端封装到一个单独的线程中,这样,就可以同时处理多个客户端请求。
示例如下:
//想要从客户端上传给服务端一张图片,因为开启服务端,操作的人数比较多,所以需要用到多线程
//底层的数据时二进制数,所以读取写入都用字节流对象
import java.io.*;
import java.net.*;
//当服务器收到多个数据的时候,先获取到对象,然后给各自的对象单开一个线程去执行,
//这样的话就不影响其他客户端向服务端发送数据
class PicThread implements Runnable
{
//每一个线程开启都有一个属于自己子的客户端对象
private Socket s;
String ip = null;
PicThread(Socket s)
{
this.s = s;
}
public void run()
{
int count = 1;
try
{
//想获取到IP地址,打印出来,看是谁连接了服务器
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"......connected");
//为了防止重名文件的覆盖操作,先判断文件对象是否存在,存在的话在括号中加一再存,
File file = new File(ip+"("+(count)+").jpg");
while(file.exists())
{
file = new File(ip+"("+(count++)+").jpg");
}
FileOutputStream fos = new FileOutputStream(file);
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = in.read(buf))!=-1)
{
fos.write(buf,0,len);
}
out.write("上传成功".getBytes());
s.close();
fos.close();
}
catch (Exception e)
{
throw new RuntimeException(ip+"上传失败");
}
}
}
class PicClient {
public static void main(String[] args) throws Exception
{
//使用在主函数中传参数的形式进行上传文件
if(args.length>1)
{
System.out.println("传入的格式不正确");
return;
}
//如果文件对象不存在或者不是文件,不进行任何操作
File file = new File(args[0]);
if(!(file.exists() && file.isFile()))
{
System.out.println("输入的文件不存在,或者文件有问题");
return;
}
//如果文件的扩展名不是JPG的,也不让上传
if(!(file.getName().endsWith(".jpg")))
{
System.out.println("文件名不合适");
return;
}
//文件过大,不然上传
if(file.length()>1024*1024*5)
{
System.out.println("文件过大,没按好心");
return;
}
Socket s = new Socket("192.168.0.104",10006);
FileInputStream fis = new FileInputStream(file);
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = fis.read(buf))!=-1)
{
out.write(buf,0,len);
}
s.shutdownOutput();
byte[] by = new byte[1024];
int l = in.read(by);
System.out.println(new String(by,0,l));
fis.close();
s.close();
}
}
class PicServer
{
public static void main(String[] args) throws Exception
{
//开启服务端对象
ServerSocket ss = new ServerSocket(10006);
//进入循环,服务端就一直不关了,进来一个客户端连接,就进循环,获取自己的对象,然后开启一个线程执行
while(true)
{
Socket s = ss.accept();
new Thread(new PicThread(s)).start();
}
}
}
运行结果:
接下来有一个需求:
客户端通过键盘录入用户名
服务端对这个用户名进行校验
如果该用户存在,在服务端显示,***已登录
并在客户端显示***欢迎光临
如果该用户不存在,在服务端显示***尝试登陆
并在客户端显示***该用户不存在
最多就登陆三次
//想要一个登陆系统,客户端可以登陆到服务器,
//每个客户端只能校验三次,
//服务端检验客户端输入的name是否正确
//正确则给客户端返回欢迎
//错误测给你客户端返回该name不存在
import java.io.*;
import java.net.*;
class LoginClient {
public static void main(String[] args) throws Exception
{
//客户端先建立Socket对象,并且获取到各种对象
Socket s = new Socket("127.0.0.1",10008);
BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
String line = null;
//校验三次,用for循环
for (int x = 0;x<3 ;x++ )
{
String name = bufr.readLine();//先获取到键盘录入的name值
if(name==null)
break;//判断如果为null就退出登录,关闭资源
out.println(name);//将从键盘读取的值发给服务器端
//读取服务器端发回来的数据
String str = in.readLine();
System.out.println(str);
//如果服务器端发回来的数据中包含欢迎,说明中了,跳出
if(str.contains("欢迎"))
break;
}
bufr.close();
s.close();
}
}
class UserThread implements Runnable
{
private Socket s;
UserThread(Socket s)
{
this.s = s;
}
public void run()
{
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip+"......conneted");
try
{
for (int x = 0;x<3 ;x++ )
{
BufferedReader bufr = new BufferedReader(new FileReader("names.txt"));
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
boolean flag = false;
String name = in.readLine();//先获取到客户端发过来的数据
if(name==null)
break;//如果读到name的值为空的,直接跳出
String line = null;
while((line = bufr.readLine())!=null)
{
//然后那读取到的值和文件中的数据一行一行进行比较
if(line.equals(name))
{
flag = true;//如果碰到一样的了,flag的值变为真,跳出执行下一条语句
break;
}
}
if(flag)
{
//入股flag的值为真,则打印登陆成功,给客户端发送数据欢迎光临,然后跳出
out.println(name+"欢迎光临");
System.out.println(name+"登陆成功");
break;
}
else
{
//如果flag的值为假的,则在服务端打印尝试登陆,给客户端发送数据用户名不存在
System.out.println(name+"尝试登陆");
out.println(name+"该用户名不存在");
}
}
s.close();
}
catch (Exception e)
{
throw new RuntimeException(ip+"校验失败");
}
}
}
class LoginServer
{
public static void main(String[] args) throws Exception
{
//同样的服务端要开启多个线程进行接收数据
ServerSocket ss = new ServerSocket(10008);
for(int x = 0;)
{
Socket s = ss.accept();
new Thread(new UserThread(s)).start();
}
}
}
运行结果:
接下来我们一浏览器作为客户端,然后用我们自己定义的服务端
现在定义自己定义的服务器:当服务器开启的时候,流浪器访问服务器,服务器给浏览器发送一句话:
//自定义服务端
import java.io.*;
import java.net.*;
public class ServerDemo {
public static void main(String[] args) throws Exception
{
//先上来肯定是建立server服务
ServerSocket ss = new ServerSocket(11000);
Socket s = ss.accept();
//获取到输出流
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
//然后给客户端谢绝一句话
out.println("客户端你好");
ss.close();
s.close();
}
}
运行结果为:
先开启服务器,然后再浏览器输入自己的IP地址,端口号:
验证成功;
接下来验证浏览器作为客户端,使用tomcat服务器,来获取tomcat中的一个资源:
我们先编写这样的一个网页:
<!doctype html>
<html>
<head>
<meta charset="gbk">
<title>这是我的主页</title>
</head>
<body>
<h1>欢迎光临</h1>
<font color="#00FF00"; size="+2">窗前明月光<font></br>
<font color="#00FF00"; size="+2">疑是地上霜<font></br>
<font color="#00FF00"; size="+2">举头望明月<font></br>
<font color="#00FF00"; size="+2">低头思故乡<font></br>
</body>
</html>
页面效果是这样的:
将该HTML文件存储在tomcat服务器的webapps\myweb目录下:
然后开启tomcat服务器:
在浏览器中输入服务器主机地址,和端口号8080;
如果出现下边的这个页面,说明tomcat服务器开启完成
在地址栏中填写下要访问的主机名,端口号,要获取的文件路径和想要获取的文件:
那么我们就想自己定义一个浏览器来获取服务器上的数据了,
在定义之前我们需要知道,浏览器到底是怎么样获取到tomcat服务器端的数据的,
也就是说,浏览器到底是给服务器发了什么样的数据才能获取到服务器端的资源的呢?
我们可以用浏览器访问自定义的服务器,在服务器端把浏览器发送的数据全部读出来,
在自定义的服务器中加以下代码:
//自定义服务端
import java.io.*;
import java.net.*;
public class ServerDemo {
public static void main(String[] args) throws Exception
{
//先上来肯定是建立server服务
ServerSocket ss = new ServerSocket(11000);
Socket s = ss.accept();
//获取到读取流
InputStream in = s.getInputStream();
//读取浏览器(客户端)发过来的消息
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf,0,len));
//获取到输出流
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
//然后给客户端谢绝一句话
out.println("客户端你好");
ss.close();
s.close();
}
}
开启服务器:在浏览器中输入主机名和端口号访问自定义的浏览器:
运行时发现:除了在浏览器获取到服务器端发的“客户端你好”之外;
服务器端也收到了一部分数据如下:
通过解读以上数据:
第一行:GET请求方式:请求获取的路径和文件:http协议版本
第二行:请求的主机名,和端口号
第三行:用户信息
第四行:支持的型号,都是用\表示的键值对形式
第五行:支持的语言:首选中文
第六行:支持的打包方式:zip打包
第七行:传送完数据之后连接保持开启
那么了解了浏览器给服务器发送的数据之后,我们就可以自己写一个浏览器了:
//定义自己的浏览器去获取tomcat服务器上的数据
import java.io.*;
import java.net.*;
public class MyIE {
public static void main(String[] args) throws Exception
{
//指定的服务器为tomcat服务器
Socket s = new Socket("127.0.0.1",8080);
BufferedReader in =new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
//像浏览器一样先给服务器发送一段请求消息头
out.println("GET /myweb/1.html HTTP/1.1");//告诉服务器要获取的路径和文件,协议
out.println("Accept: */*");
out.println("Accept-Language: zh-cn");//指定文字是中文
out.println("Host: 127.0.0.1:8080");//指定主机为该IP地址下的8080端口
out.println("Connection: closed");//传送完消息就关闭连接
out.println();//记住:消息头和正文之间要有一行空行
//还要读取服务器端发回来的数据
String line = null;
while((line = in.readLine())!=null)
{
System.out.println(line);
}
s.close();
}
}
使用自己定义的浏览器去访问tomcat服务器,获取到服务器上的资源,然后读取到服务器给自定义浏览器发回来的数据:
可以看出来:tomcat服务器在发送文件之间,还给客户端发送了一个http应答消息头
了解了这样的原理之后,我们就可以将自定义的浏览器和GUI结合起来,
在输入框中输入主机地址端口好请求文件,在文本区域中出现请求到的数据:
整体思想是:将文本框中输入的数据,进行切割,将切割下来的数据进行封装成一个个的对象,再按照浏览器给服务端写的数据的方式写出去数据
再定义读取流将服务器端的数据读取出来加在文本区域中
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
class MyIEByGUI
{
MyIEByGUI()
{
inis();
}
//在外边建立所有对象的引用,省的等会找不到还是怎么的
Frame f = null;
Button b = null;
TextField tf = null;
TextArea ta = null;
public void inis()
{
f = new Frame("我的Window");
b = new Button("转到");
tf = new TextField(55);
ta = new TextArea(40,62);
f.setBounds(300,200,600,500);//该方法取代的setSize和setLocation设置的值分别表示横坐标,纵坐标,宽和高
f.setLayout(new FlowLayout());
f.add(tf);
f.add(b);
f.add(ta);
myListener();//该方法可以添加进来全部的监听器
f.setVisible(true);
}
//这里添加监听器
public void myListener()
{
//关闭键关闭窗体的监听器
f.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
//给转到按钮添加时间监听器
b.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
try
{
showDir();
}
catch (Exception ex)
{
}
}
});
//键盘监听器,输完目录之后按回车可以直接显示出服务器上的资源
tf.addKeyListener(new KeyAdapter()
{
public void keyPressed(KeyEvent e)
{
if(KeyEvent.VK_ENTER==e.getKeyCode())
{
try
{
showDir();
}
catch (Exception ex)
{
}
}
}
});
}
//这里封装的是按照输入框中的数据,获取到服务器端资源的方法
private void showDir() throws Exception
{
//每一次运行的时候,都把文本区域中的数据清空
ta.setText("");
String line = tf.getText();//http://127.0.0.1:8080/myweb/1.html
//因为里边有//斜杠会有转义作用,所以我们可以使用获取角标的方式
//获取主机和端口的字符串
int index1 = line.indexOf("//")+2;
int index2 = line.indexOf("/",index1);
String str = line.substring(index1,index2);
//接下里获取资源路径
String path = line.substring(index2);//从index2以后全部截出来
//将主机和端口的字符串切开:
String[] strs = str.split(":");
String host = strs[0];
int port = Integer.parseInt(strs[1]);
Socket s = new Socket(host,port);
BufferedReader in =new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter out = new PrintWriter(s.getOutputStream(),true);
//像浏览器一样先给服务器发送一段请求消息头
out.println("GET "+path+" HTTP/1.1");//告诉服务器要获取的路径和文件,协议
out.println("Accept: */*");
out.println("Accept-Language: zh-cn");//指定文字是中文
out.println("Host: 127.0.0.1:8080");//指定主机为该IP地址下的8080端口
out.println("Connection: closed");//传送完消息就关闭连接
out.println();//记住:消息头和正文之间要有一行空行
String s1 = null;
while((s1 = in.readLine())!=null)
{
ta.append(s1+"\r\n");
}
s.close();
}
}
public class MyIEByGUIDemo {
public static void main(String[] args)
{
new MyIEByGUI();
}
}
运行结果:
那么我们就发现了,自定定义的浏览器获取到的数据就带着那些前边的应答消息头,
但是电脑上的浏览器怎么就不显示这些东西呢?
这是因为,我们是使用TCP协议进行数据的传输的,而TCP是在传输层面上
浏览器是在应用层面上,应用层面上的浏览器获取到数据之后可以对传输层的应答消息头进行解析,并且去掉他,所以浏览器不会显示应答消息头,而我们自定义的浏览器不会对应答消息头进行解析。
而我们也意识到,在获取主机名,端口号,资源路径,文件名的时候,进行的切割动作非常麻烦,聪明的人都会将这样的动作封装到一个类里边
这些都不用你做,java中有自己的类URL,通过构造方法可以将字符串路径封装一个网页访问的路径
import java.net.*;
public class URLDemo {
public static void main(String[] args) throws Exception
{
//资源定位符,表示获取服务器中的资源的指定路径
URL url = new URL("http://127.0.0.1:8080/myweb/1.html?name=zhangsan&age=43");
//可以获取到资源的协议
System.out.println("getPRotocol:"+url.getProtocol());
//获取到资源的主机
System.out.println("getHost:"+url.getHost());
//可以获取到资源的端口号
System.out.println("getPort:"+url.getPort());
//可以获取到资源的文件路径包括文件名
System.out.println("getPath:"+url.getPath());
//可以获取到资源的路径和文件名包括文件中提交的信息
System.out.println("getFile:"+url.getFile());
//可以获取到文件中提交的信息
System.out.println("getQuery:"+url.getQuery());
}
}
运行结果:
这里需要注意的是getPort方法,返回的端口号,我们一般输入的路径里边没有端口号,这是因为getPort方法在进行封装的时候会判断,如果getPort方法返回的是-1 的话,就默认port的值为80(web端口号)
那么有了这个对象,我们还可以干点什么事情呢?
可以通过openConnection方法对封装到里边的链接远程连接,返回一个URLConnection的连接对象,
该类里边封装了一Socket对象,可以通过URLConnection对象获取到该客户端的读取流和写出流
//通过openConnection方法获取到远程连接对象
import java.net.*;
public class ConnectionDemo {
public static void main(String[] args) throws Exception
{
URL url = new URL("http://127.0.0.1:8080/myweb/1.html");
URLConnection conn = url.openConnection();
System.out.println(conn);
}
}
打印的结果为:
看的出来,里边封装的是http协议的,也就是说该对象是在应用层上,
这说明什么呢?从传输层获取到的数据可以在应用层被拆包解析
既然说URLConnection里边封装了Socket对象了,那就可以获取到Socket中的流对象了
以之前的GUI自定义浏览器为例,修改代码,使用URLConnection对象:
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
class MyIEByGUI2
{
MyIEByGUI2()
{
inis();
}
//在外边建立所有对象的引用,省的等会找不到还是怎么的
Frame f = null;
Button b = null;
TextField tf = null;
TextArea ta = null;
public void inis()
{
f = new Frame("我的Window");
b = new Button("转到");
tf = new TextField(55);
ta = new TextArea(40,62);
f.setBounds(300,200,600,500);//该方法取代的setSize和setLocation设置的值分别表示横坐标,纵坐标,宽和高
f.setLayout(new FlowLayout());
f.add(tf);
f.add(b);
f.add(ta);
myListener();//该方法可以添加进来全部的监听器
f.setVisible(true);
}
//这里添加监听器
public void myListener()
{
//关闭键关闭窗体的监听器
f.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
//给转到按钮添加时间监听器
b.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
try
{
showDir();
}
catch (Exception ex)
{
}
}
});
//键盘监听器,输完目录之后按回车可以直接显示出服务器上的资源
tf.addKeyListener(new KeyAdapter()
{
public void keyPressed(KeyEvent e)
{
if(KeyEvent.VK_ENTER==e.getKeyCode())
{
try
{
showDir();
}
catch (Exception ex)
{
}
}
}
});
}
//这里封装的是按照输入框中的数据,获取到服务器端资源的方法
private void showDir() throws Exception
{
//每一次运行的时候,都把文本区域中的数据清空
ta.setText("");
String line = tf.getText();//http://127.0.0.1:8080/myweb/1.html//没网了,那个地址不管用了=_=
URL url = new URL(line);
//连接到远程服务器
URLConnection conn = url.openConnection();
//获取到读取流对象
InputStream in = conn.getInputStream();
//对服务器发回来的数据进行读取
byte[] buf =new byte[1024];
int len = in.read(buf);
ta.setText(new String(buf,0,len));
}
}
public class MyIEByGUIDemo2 {
public static void main(String[] args)
{
new MyIEByGUI2();
}
}
运行结果为:
一些小知识点:
Socket对象在new对象的时候,里边要封装一个IP地址,和一个端口号,
但是Socket也可以对空参数的进行构造,构造之后可以通过connect方法进行连接,connect方法的参数是一个InetSocketAddress对象,而InetSocketAddress对象在初始化的时候就要指定一个IP地址和端口号。
一样的道理
ServerSocket对象在创建的时候要指定一个端口号,
也可以指定一个端口号和一个int类型的服务器可以连接的最大客户端数目。
域名解析
当我们在浏览网页的时候,比如新浪,我们在地址栏中输的是http://www.sina.com.cn
那么我们输入的这样的网址怎么就找到了新浪的主机IP地址了呢?
是这样的,访问任何一个主机的时候,他会先连接到公网上的一个DNS服务器,
DNS服务器上有主机名和IP地址的映射关系,
连上DNS服务器之后,服务器返回来的一个IP地址,浏览器再按照IP地址去连接服务器。
这样做省下了用户记忆每个网站IP地址的麻烦,只要记住域名就可以了
所以我要访问新浪网站走的过程是这样子的:
那么就会问了,自带的回环IP地址127.0.0.1 对应的是localhost名
难道这也连接都公网去找吗?
不是的,本机自带的回环地址,映射关系是存在于本机上的,在电脑的
C:\Windows\System32\drivers\etc目录下的hosts文件就是记录这样的映射关系的:
当然了,这个值也是可以改变的,
主要原理就是:当浏览器要访问一个主机名的时候,先在本机的该目录下找有没有该主机名对应的IP地址的映射关系,
如果有,就走的是本地的地址,如果没有,就走了公网上的DNS服务器去找映射关系。
所以如果我把在本地把新浪的主机IP地址对应成127.0.0.1
在浏览器中访问新浪的时候,走的就是我本机的服务器了:
这个过程走的就是,在浏览器中有www.sina.com.cn,浏览器再本地一找
找到对应的地址是127.0.0.1
就走了127.0.0.1这个地址了,所以会出现上图显示的结果。