网络:一组相互连接的计算机
OSI参考模型: 开放系统互连参考模型(Open System Interconnect)
OSI模型其实现实生活中并不会用到,现实生活中用到的是下面的四层模型
TCP/IP参考模型:传输控制/网际协议(Transfer Control Protocol/Internet Protocol)
特殊的IP地址
端口:
端口是一个虚拟的概念,并不是说在主机上真的有若干个端口,通过端口,可以在一个主机上运行多个网络应用程序
传输协议:
UDP: 相当于发短信(有字数限制),不需要建立连接,数据包的大小限制在64kb,效率较高,不安全,容易丢包
TCP:相当于打电话,需要建立连接,效率相对比较低,数据传输安全,三次握手完成.(点名—答道—确认)
注意:UDP和TCP在使用的时候并不一定都是分开使用的,它们之间可以混合使用,例如视频通话的时候就是使用TCP先开始建立连接,在连接建立后再通过UDP进行传输.如果全程都只用TCP进行传输,将会耗费特别大的数据量
Socket套接字
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket.
Java中使用Socket完成TCP程序的开发,使用此类可以方便的建立可靠的、双向的、持续性的、点对点的通讯连接.
在Socket的程序开发中,服务器端使用ServerSocket等待客户端的连接.
对于java的网络程序来讲,每一个客户端都使用一个Socket对象表示
基于TCP协议的Socket编程
进行网络通信时,Socket需要借助数据流来完成数据的传递工作
练习:建立一个客户端和服务端,并且客户端向服务端发送一条消息
public class Client {
public static void main(String[] args) {
try {
Socket client;
//创建socket对象,其实开启实现io的虚拟接口(此接口不是java中的接口,而是类似于网线的插槽)
//需要指定数据接收方的ip地址和端口号
client = new Socket("localhost",10086);
//获取输出流对象,向服务端发送数据
OutputStream out = client.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(out);
dataOutputStream.writeUTF("hello,server");
dataOutputStream.close();
out.close();
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args) {
try {
//服务端需要使用serverSocket来开放本地的端口
ServerSocket serverSocket = new ServerSocket(10086);
//需要接受client传输过来的数据,需要定义socket对象
Socket server = serverSocket.accept();
//通过server获取输入流对象
InputStream in = server.getInputStream();
DataInputStream dataInputStream = new DataInputStream(in);
String str= dataInputStream.readUTF();
System.out.println(str);
//关闭流操作
dataInputStream.close();
in.close();
server.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在运行的时候记得先启动服务端,再启动客户端
接下来是服务端在接收到客户端信息的时候返回响应信息的写法
public class Client {
public static void main(String[] args) {
try {
//创建客户端的套接字
Socket client = new Socket("127.0.0.1",10000);
//获取输出流对象
OutputStream out = client.getOutputStream();
//数据输出
out.write("hello Server,are you OK?".getBytes());
//获取输入流对象
InputStream in = client.getInputStream();
byte[] bytes = new byte[1024];
int len = in.read(bytes);
System.out.println(new String(bytes,0,len));
//关闭流
in.close();
out.close();
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Server {
public static void main(String[] args) {
try {
//服务端需要使用serverSocket来开放本地的端口
ServerSocket serverSocket = new ServerSocket(10000);
//获取服务端的套接字对象
Socket server = serverSocket.accept();
//获取输入流对象
InputStream in = server.getInputStream();
byte[] bytes = new byte[1024];
int len = in.read(bytes);
System.out.println(new String(bytes,0,len));
//给客户端响应
OutputStream out = server.getOutputStream();
out.write("I'm fine ! thanks Client".getBytes());
//关闭流
out.close();
in.close();
server.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
到目前为止我们已经完成了客户端和服务端的建立,此时的程序就像个聊天室,而且该聊天室只允许两个人存在,而且只能发一条消息,那我们该怎样做才能让聊天室人数更多且能发更多消息呢?
我们先来写一下从客户端往服务端传输图片的程序
public class ClientPic2 {
public static void main(String[] args) throws IOException {
//创建图片的输入流对象
FileInputStream fileInputStream = new FileInputStream("mePic.jpg");
//创建Socket
Socket client = new Socket("localhost",10089);
//获取输出流对象
OutputStream outputStream = client.getOutputStream();
int tmp = 0;
while ((tmp = fileInputStream.read())!=-1){
outputStream.write(tmp);
}
client.shutdownOutput();//添加输出流完成的标志
InputStream inputStream = client.getInputStream();
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
System.out.println(new String(bytes,0,len));
client.shutdownInput();//添加输入流完成的标志
//关闭操作
inputStream.close();
outputStream.close();
fileInputStream.close();
client.close();
}
}
public class ServerPic {
public static void main(String[] args) throws IOException {
//创建服务端对象,开放端口
ServerSocket serverSocket = new ServerSocket(10089);
//创建服务端的Socket
Socket server = serverSocket.accept();
//获取输入流对象
InputStream inputStream = server.getInputStream();
//创建文件输出流对象
FileOutputStream fileOutputStream = new FileOutputStream("meCopy.jpg");
int tmp = 0;
while((tmp = inputStream.read())!=-1){
fileOutputStream.write(tmp);
}
server.shutdownInput();//添加输入流完成的标志
//上传如片结束后给予客户端响应
OutputStream outputStream = server.getOutputStream();
outputStream.write("上传成功!".getBytes());
server.shutdownOutput();//添加输出流完成的标志
outputStream.close();
fileOutputStream.close();
inputStream.close();
server.close();
serverSocket.close();
}
}
上面的程序我们使用到的shutdownInput()和shutdownOutput()是必须要手动添加的,一来它的作用是告诉程序已经传输完毕;二来,在以后涉及到多线程的时候,如果每个线程都只使用不释放的话,内存很容易溢出
练习: 实现用户登录
需求说明:
客户端序列化对象
服务器端反序列化对象
我们在io流中,如果想传输一个对象,必须使用ObjectInputStream、ObjectOutputStream
但是这个对象在实际使用过程中我们需要先实现一个序列化的值,这个序列化的值需要通过我们的序列化接口提前创建好.序列化接口中有一个单独的ID值.这个ID值在使用过程中我们是要保持一致的.
在实际的网站登录过程中,情况是登录失败时返回错误提示,然后用户可以继续尝试登录,而对服务端不会有影响
public class User implements Serializable {
//程序自动生成的ID
private static final long serialVersionUID = 4594787634610567556L;
private String userName;
private String password;
public String getUserName() {
return userName;
}
}
上面的ID值其实是程序自动生成的,如何让程序自动生成这个ID值呢,我们先来设置一下我们的IDEA
然后对着我们的类型使用快捷键(fn+option+enter)即可出现设置UID的选择
接下来我们先编写客户端
public class Client {
public static void main(String[] args) throws IOException {
Socket client = new Socket("localhost",10009);
OutputStream outputStream = client.getOutputStream();
User user = getUser();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(user);
//调用shutdown方法告诉对方传输完成
client.shutdownOutput();
//接收服务端发出的响应
DataInputStream dataInputStream = new DataInputStream(client.getInputStream());
String str = dataInputStream.readUTF();
System.out.println(str);
//关闭流
dataInputStream.close();
objectOutputStream.close();
outputStream.close();
client.close();
}
public static User getUser(){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String userName = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
return new User(userName,password);
}
}
编写服务端
public class Server {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ServerSocket serverSocket = new ServerSocket(10009);
Socket server = serverSocket.accept();
//获取输入流对象
InputStream inputStream = server.getInputStream();
//需要使用ObjectInputStream对象
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
//使用User对象进行接收,强制转换
User user = (User) objectInputStream.readObject();
String str = "";
if (user.getUserName().equals("msb") && user.getPassword().equals("psv")){
System.out.println(user.getUserName+" 登录了系统")
str = "登录成功";
}else {
str = "登录失败";
}
//调用shutdown方法告知程序接收完成
server.shutdownInput();
//向客户端发出响应信息
DataOutputStream outputStream = new DataOutputStream(server.getOutputStream());
outputStream.writeUTF(str);
server.shutdownOutput();
//关闭流
outputStream.close();
objectInputStream.close();
inputStream.close();
server.close();
serverSocket.close();
}
}
由于目前我们是在同一个IDEA里面编写的,都客户端序列化对象和服务端反序列化对象都调用的是同一个User类,并不会发现此时User类中serialVersionUID的作用.若我们是在两个IDEA或两台电脑上分别运行客户端和服务端,此时我们两台电脑中的User类的serialVersionUID就必须要保持一致了,不然会程序运行失败
运行上面的程序,我们此时可以发现,登录功能已经实现了,但服务端还是只接收了一次客户端的请求后就关闭了,我们可以使用什么方式来是服务端保持一直开启且能一直接收客户端请求呢?
我们此时可以使用while循环,也可以使用多线程的方式
我们先来使用while循环的方式来实现看看
public class Client2 {
public static void main(String[] args) throws IOException {
boolean flag = false;
while (!flag){
Socket client = new Socket("localhost",10009);
OutputStream outputStream = client.getOutputStream();
User user = getUser();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(user);
//调用shutdown方法告诉对方传输完成
client.shutdownOutput();
//接收服务端发出的响应
DataInputStream dataInputStream = new DataInputStream(client.getInputStream());
String str = dataInputStream.readUTF();
System.out.println(str);
if (str.equals("登录成功")){
flag=true;
}
dataInputStream.close();
objectOutputStream.close();
outputStream.close();
client.close();
}
}
public static User getUser(){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String userName = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
return new User(userName,password);
}
}
public class Server2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ServerSocket serverSocket = new ServerSocket(10009);
while (true){
Socket server = serverSocket.accept();
//获取输入流对象
InputStream inputStream = server.getInputStream();
//需要使用ObjectInputStream对象
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
//使用User对象进行接收,强制转换
User user = (User) objectInputStream.readObject();
String str = "";
if (user.getUserName().equals("msb") && user.getPassword().equals("psv")){
System.out.println("用户: "+user.getUserName()+" 登录了系统");
str = "登录成功";
}else {
str = "登录失败";
}
//调用shutdown方法告知程序接收完成
server.shutdownInput();
//向客户端发出响应信息
DataOutputStream outputStream = new DataOutputStream(server.getOutputStream());
outputStream.writeUTF(str);
server.shutdownOutput();
//关闭流
outputStream.close();
objectInputStream.close();
inputStream.close();
server.close();
}
// serverSocket.close();
}
}
接下来我们使用多线程的方式来实现
public class LoginThread implements Runnable{
private Socket socket;
public LoginThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
ObjectInputStream objectInputStream = null;
DataOutputStream outputStream = null;
try {
objectInputStream = new ObjectInputStream(socket.getInputStream());
User user = (User)objectInputStream.readObject();
String str = "";
if (user.getUserName().equals("msb") && user.getPassword().equals("psv")){
System.out.println("用户: "+user.getUserName()+" 登录了系统");
str = "登录成功";
}else {
str = "登录失败";
}
//调用shutdown方法告知程序接收完成
socket.shutdownInput();
//向客户端发出响应信息
outputStream = new DataOutputStream(socket.getOutputStream());
outputStream.writeUTF(str);
socket.shutdownOutput();
//关闭流
// inputStream.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
outputStream.close();
objectInputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class Server3 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(10009);
while (true){
Socket socket = serverSocket.accept();
LoginThread loginThread = new LoginThread(socket);
new Thread(loginThread).start();
}
}
}
接下来我们编写UDP的传输,UDP实际我们编程中应用的比较少,在此作为了解即可
public class UDPClient {
public static void main(String[] args) throws IOException {
//创建UDP通信的socket
DatagramSocket datagramSocket = new DatagramSocket(10009);
//从控制台读取数据
Scanner scanner = new Scanner(System.in);
System.out.println("请输入传输内容: ");
String str = scanner.nextLine();
//需要传输的包,包里面添加的是服务端的地址和端口号
DatagramPacket packet = new DatagramPacket(str.getBytes(),str.getBytes().length, InetAddress.getByName("localhost"),10008);
datagramSocket.send(packet);
datagramSocket.close();
}
}
public class UDPServer {
public static void main(String[] args) throws IOException {
DatagramSocket datagramSocket = new DatagramSocket(10008);
byte[] buf = new byte[1024];
//用来接受传输过来的数据
DatagramPacket datagramPacket =new DatagramPacket(buf,buf.length);
//利用创建好的数据报包对象来接受数据
datagramSocket.receive(datagramPacket);
//打印输出信息
System.out.println(new String(datagramPacket.getData(),0,datagramPacket.getLength()));
datagramSocket.close();
}
}