一、Socket简介
1.Socket概述
Java最初是作为网络编程语言出现的,它对网络的高度支持,使得客户端和服务器流畅的沟通变成现实。而在网络编程中,使用最多的就是Socket,每一个实用的网络程序都少不了它的身影。
在计算机网络编程技术中,两个进程,或者说两台计算机可以通过一个网络通信实现数据的交换,这种通信链路的端点就被称为“套接字”(即Socket),Socket是网络驱动层提供给应用程序的接口或者说是一种机制。举一个物流快递的例子来说明Socket,发件人将写有收货地址信息的货物送到快递站,发件人不用关心物流是如何进行的,货物被送到收货人所在地区的快递点,进行配送,收货人等待收货就可以了,这个过程很形象地说明了信息在网络中传递的过程。其中货物就是信息,2个快递点就是2个端点Socket。信息在网络中寻址传递,应用程序并不关心,只负责准备发送数据和接收数据。
2.Socket通信原理
对于编程人员来说,无需了解Socket底层机制是如何传送数据的,而是直接将数据提交给Socket,Socket会根据应用程序提供相关的信息。通过一系列计算,绑定IP及信息数据,将数据交给驱动程序向网络上发送出去。
Socket的底层机制非常复杂,Java平台提供了一些虽然简单但相当强大的类,可以简单地有效地使用Socket开发通信程序而无需了解底层机制。
3. java.net包
java.net包提供了若干支持基于套接字的客户端/服务器通信的类。
java.net包中常用的类有Socket、ServerSocket、DatagramPacket、DatagramSocket、InetAddress、URL、URLConnection和URLEncoder等。
为了监听客户端的连接请求,可以使用ServerSocket类。Socket类实现用于网络上进程通信的套接字。DtatagramSocket类使用UDP协议实现客户端和服务器套接字。DatagramPacket类使用DatagramSocket类的对象封装设置和收到的数据报。InetAddress类表示Internet地址。在创建数据报文和Socket对象时,可以使用InetAdress类。
二、基于TCP协议的Socket编程
java.net包的两个类Socket和ServerSocket,分贝用来实现双向安全连接的客户端和服务器端,他们是基于TCP协议进行工作的,它的工作过程如同打电话的过程,只有双方都接通了,才能开始通话。
进行通信时,Socket需要借助数据流来完成数据的传递工作。如果一个应用程序要通过网络向另一个应用程序发送数据,只要简单的创建Socket,然后将数据写入到与该Socket关联的输出流即可。对应的,接受方的应用程序创建Socket,从相关的输入流读取数据即可。
1. Socket类
Socket类在客户端和服务器端之间建立连接。可用Socket类的构造方法创建套接字,并将此套接字连接至制定的主机和端口。
- 构造方法
- Socket s = new Socket(hostName,port); hostName:主机名,port:端口号。创建对象时可能会抛出UnknownHostException或IOException异常,必须捕获它们。
- Socket s = new Socket(address,port); address:InetAddress对象。创建对象时可能会抛出UnknownHostException或IOException异常,必须捕获它们。
- 常用方法
方法名 | 说明 |
InetAddress getInetAddress() | 返回与Socket对象关联的InetAddress |
int getPort() | 返回此Socket对象所连接的远程端口 |
int getLocalPort | 返回此Socket对象所连接的本地端口 |
InputStream getInputStream() | 返回与此套接字关联的InputStream |
OutputStream getOutputStream() | 返回与此套接字关联的OutputStream |
void close() | 关闭该Socket |
2.ServerSocket类
ServerSocket对象等待客户端建立连接,连接建立后进行通信。
- 构造方法
- ServerSocket s = new ServerSocket(port); port:端口号。创建对象时可能会抛出IOException异常,必须捕获它们。
- ServerSocket s = new ServerSocket(port,maxqu); maxqu:最大队列长度。队列长度表示系统在拒绝连接前可以拥有的最大客户端连接数。
- 常用方法
Socket类中列出的方法同样适用于ServerSocket,此外,ServerSocket类具有accept()方法,此方法用于等待客户端发起通信,这样Sockt对象就可以用于进一步的数据传输。
下面是一个简单例子:
package cn.yu.SocketDemo; import java.io.*; import java.net.*; public class TCPSever { /**
* 服务器端 * @param args * @throws IOException */ public static void main(String[] args) throws IOException { // TODO Auto-generated method stub //使用ServerSocket ServerSocket server = new ServerSocket(8000); //每个用户在程序中就是一个Socket Socket client = null; //等待客户端连接 client = server.accept(); //像客户端打印信息 PrintWriter out = null; //准被向客户端打印信息 out = new PrintWriter(client.getOutputStream()); out.println("我是服务器端,Hello World");
//关闭流和Socket对象 out.close(); client.close(); server.close(); } }
package cn.yu.SocketDemo; import java.io.*; import java.net.*; public class TCPClient { /**
* 客户端 * @param args * @throws IOException * @throws UnknownHostException */ public static void main(String[] args) throws UnknownHostException, IOException { // TODO Auto-generated method stub //表示一个客户端的Socket Socket client = null; //表示一个客户端的输入信息 BufferedReader buf = null; client = new Socket("localhost",8000); buf = new BufferedReader(new InputStreamReader(client.getInputStream())); System.out.println(buf.readLine());
//关闭流和Socket对象 buf.close(); client.close(); } }
注意,先执行服务器端代码,在执行客户端代码。
三、使用Socket编程实现用户登录
1.实现单用户登陆
Socket编程一般分为4个步骤:
- 建立连接
- 打开Socket关联的输入/输出流
- 从数据流中读写信息
- 关闭所有的数据流和Socket
package cn.yu.SocketDemo; import java.io.Serializable; /* * 用户类:要传递的信息 */ public class User implements Serializable { private String loginName; //用户名 private String pwd; //用户密码 public User(String loginName,String pwd){ super(); this.loginName = loginName; this.pwd = pwd; } //getter/setter方法 public String getLoginName() { return loginName; } public void setLoginName(String loginName) { this.loginName = loginName; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } }
package cn.yu.SocketDemo; import java.io.*; import java.net.*; public class TCPClient { /** * 客户端 * @param args * @throws IOException * @throws UnknownHostException */ public static void main(String[] args) throws UnknownHostException, IOException { // TODO Auto-generated method stub //表示一个客户端的Socket,制定服务器的位置及端口 Socket client = new Socket("localhost",8000);; //打开输入/输出流 OutputStream os = client.getOutputStream(); InputStream is = client.getInputStream(); //对象序列化 ObjectOutputStream oos = new ObjectOutputStream(os); //发送客户端登陆信息,即向输出流中写入信息 User user = new User(); user.setLoginName("lipengfei"); user.setPwd("123456"); oos.writeObject(user); client.shutdownOutput(); //表示一个客户端的输入信息 BufferedReader buf = new BufferedReader(new InputStreamReader(is)); String reply = null; while((reply = buf.readLine())!=null){ System.out.println("我是客户端,服务器的响应是:"+reply); } buf.close(); oos.close(); is.close(); os.close(); client.close(); } }
package cn.yu.SocketDemo; import java.io.*; import java.net.*; public class TCPSever { /** * 服务器端 * @param args * @throws IOException */ public static void main(String[] args) throws IOException,ClassNotFoundException { // TODO Auto-generated method stub //表示一个客户端的Socket(ServerSocket),指定端口并开始监听 ServerSocket serverSocket = new ServerSocket(8000); //使用accept()方法等待客户端连接,每个用户在程序中就是一个Socket Socket socket = serverSocket.accept(); //打开输入/输出流 InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); //反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); //从客户端获取信息,即从输入流读取信息 User user = (User)objectInputStream.readObject(); if(!(user==null)){ System.out.println("我是服务器,客户登录信息为:"+user.getLoginName()+","+user.getPwd()); } //给客户端一个响应,即向输出流中写入信息 String reply = "欢迎你"+user.getLoginName()+",登陆成功!"; outputStream.write(reply.getBytes()); //关闭资源 outputStream.close(); inputStream.close(); socket.close(); serverSocket.close(); } }
这个例子采用一问一答的模式,先启动服务器进入监听状态,等待客户端的连接请求,连接成功后,客户端先“发言”,服务器给予“回应”。
2.实现多客户端用户登录
一个服务器不可能只针对一个客户端服务,一般是面向很多的客户端同时提供服务,但是单线程实现必然是这样的结果。解决这个问题的办法是采用多线程的方式,可以在"服务器端"创建一个专门负责监听的应用主服务程序、一个专门负责响应的线程程序。这样就可以利用多线程处理多个请求。
package cn.yu.SocketDemo; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.OutputStream; import java.net.Socket; /* * 线程类 */ public class LoginThread extends Thread{ Socket socket = null; //每启动一个线程,连接对应的Socket public LoginThread(Socket socket){ this.socket = socket; } @Override public void run() { try{ //打开输入/输出流 InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream(); //反序列化 ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); //从客户端获取信息,即从输入流读取信息 User user = (User)objectInputStream.readObject(); if(!(user==null)){ System.out.println("我是服务器,客户登录信息为:"+user.getLoginName()+","+user.getPwd()); } //给客户端一个响应,即向输出流中写入信息 String reply = "欢迎你"+user.getLoginName()+",登陆成功!"; outputStream.write(reply.getBytes()); //关闭资源 outputStream.close(); inputStream.close(); socket.close(); //serverSocket.close(); }catch (Exception e){ e.printStackTrace(); } } }
package cn.yu.SocketDemo; import java.io.*; import java.net.*; public class TCPSever { /** * 服务器端 * @param args * @throws IOException */ public static void main(String[] args) throws IOException,ClassNotFoundException { // TODO Auto-generated method stub //表示一个客户端的Socket(ServerSocket),指定端口并开始监听 ServerSocket serverSocket = new ServerSocket(8000); //使用accept()方法等待客户端连接,每个用户在程序中就是一个Socket Socket socket = null; //监听一直进行中 while(true){ socket = serverSocket.accept(); LoginThread loginThread = new LoginThread(socket); loginThread.start(); } } }
package cn.yu.SocketDemo; import java.io.*; import java.net.*; import java.util.*; public class TCPClient { /** * 客户端 * @param args * @throws IOException * @throws UnknownHostException */ public static void main(String[] args) throws UnknownHostException, IOException { // TODO Auto-generated method stub for (int i = 0; i < 5; i++) { //表示一个客户端的Socket,指定服务器的位置及端口 Socket client = new Socket("localhost",8000); //打开输入/输出流 OutputStream os = client.getOutputStream(); InputStream is = client.getInputStream(); //对象序列化 ObjectOutputStream oos = new ObjectOutputStream(os); //发送客户端登陆信息,即向输出流中写入信息 User user = new User(); user.setLoginName("lipengfei"+i); String pwdRandom = "@yu"+(new Random().nextInt(100)+1); user.setPwd(pwdRandom); oos.writeObject(user); client.shutdownOutput(); //接收服务器端的响应,即从输入流中读取数据 BufferedReader buf = new BufferedReader(new InputStreamReader(is)); String reply = null; while((reply = buf.readLine())!=null){ System.out.println("我是客户端,服务器的响应是:"+reply); } //关闭资源 buf.close(); oos.close(); is.close(); os.close(); client.close(); } } }
3.InetAddress类
java.net包中的InetAddress类用于封装IP和DNS。要创建InetAddress类的实例,可以使用工厂方法,因为此类没有可用的构造方法。
方法名 | 说明 |
static InetAddress getLocalHost() | 返回表示本地主机的InetAddress对象 |
static InetAddress getByName(String hostName) | 返回指定主机名为hostName的InetAddress对象 |
static InetAddress[] getAllByName(String hostName) | 返回指定主机名为hostName的所有可能的InetAddress对象组 |
如果找不到主机,两种方法都将抛出UnknowHostNameException异常。
package cn.yu.SocketDemo; import java.net.InetAddress; import java.net.UnknownHostException; /* * 输出本地主机的地址 */ public class InetAddressTest { public static void main(String[] args) { try{ InetAddress inetAddress = InetAddress.getLocalHost(); System.out.println("本地主机的地址是:"+inetAddress); //输出结果:域名/IP }catch (UnknownHostException e){ e.printStackTrace(); } } }
四、基于UDP协议的Socket编程
基于TCP协议的通信是安全的,是双向的,如同拨打10086服务电话,需要现有服务端,建立双向连接后,才开始数据通信,而UDP的网络通信就不一样了,只需要指明对方地址,然后将数据送出去,并不会事先连接。这样的网络通信是不安全的,所以应用在如聊天系统、咨询系统等场合下。
先了解下"数据报",是表示通信的一种报文类型,使用数据报进行通信时无须事先建立连接,它是基于UDP协议进行的。
Java中有2个类可以使用数据报实现通信的类,即DatagramPacket和DatagramSocket类。DatagramPacket起到数据容器的作用,DatagramSocket用于发送或接收DataPacket。
DatagramPacket不提供发送或接收数据的方法,而DatagramSocket类提供send()和recieve()方法,用于通过套接字发送和接收数据报。
1. DatagramPacket类
- 构造方法
客户端向外发送数据,必须首先创建一个DatagramPacket对象,在使用DatagramSocket对象发送。
构造方法 | 说明 |
DatagramPacket(byte[] data,int size) | 构造DatagramPacket对象,封装长度为size的数据包 |
DatagramPacket(byte[] buf,int length,InetAddress address,int port) | 构造DatagramPacket对象,封装长度为length的数据包并发送到指定的主机,端口号 |
- 常用方法
方法 | 说明 |
byte[] getData() | 返回字节数组,该数组包含接收到或要发送的数据报中的数据 |
int getLength() | 返回发送或接收到的数据的长度 |
InetAddress getAddress() | 返回发送或接收此数据报的主机的IP地址 |
int getPort() | 返回发送或接收此数据报的主机的端口号 |
2. DatagramSocket类
- 构造方法
DatagramSocket类不维护连接状态,不产生输入/输出数据流,它的唯一作用就是接收和发送DatagramPacket对象封装好的数据报。
构造方法 | 说明 |
DatagramSocket() | 创建一个DatagramSocket对象,并将其与本机地址上任何可用的端口绑定 |
DatagramSocket(int port) | 创建一个DatagramSocket对象,并将其与本机地址上指定的端口绑定 |
- 常用方法
方法 | 说明 |
void connect(InetAddress address,int port) | 将当前DatagramSocket对象连接到远程地址的指定端口 |
void close() | 关闭当前DatagramSocket对象 |
void disconnect() | 断开当前DatagramSocket对象的连接 |
int getLocalPort() | 返回当前DatagramSocket对象绑定的本地主机的端口号 |
void send(DatagramPacket p) | 发送指定的数据报 |
void receive(DatagramPacket p) | 接收数据报。收到数据报以后,存放在指定的DatagramPacket对象中 |
3. 使用Socket编程实现客户咨询
利用UDP通信的两个端点是平等的,也就是说通信的两个程序关系是对等的,没有主次之分,甚至他们的代码都可以完全一样,这一点要与基于TCP协议的Socket程序区分开来。
基于UDP协议的Socket网络编程一般可以分为4步:
- 利用DatagramPacket对象封装数据包;
- 利用Datagramsocket对象发送数据包;
- 利用DatagramSocket对象接收数据包;
- 利用DatagramPacket对象处理数据包;
package cn.yu.SocketDemo; import java.io.IOException; import java.net.*; public class Send { public static void main(String[] args) { InetAddress ia=null; DatagramSocket ds=null; try{ String mess="你好,我想咨询一个问题。"; //显示与本地对话框 System.out.println("我 说:"+mess); //获取本地主机地址 ia=InetAddress.getByName("localhost"); //创建DatagramPacket对象,封装数据 DatagramPacket dp=new DatagramPacket(mess.getBytes(),mess.getBytes().length ,ia,8800); //创建DatagramSocket对象,向服务器发送数据 ds=new DatagramSocket(); ds.send(dp); byte[] buf=new byte[1024]; DatagramPacket dpre=new DatagramPacket(buf,buf.length); //创建DatagramSocket对象,接收数据 //ds=new DatagramSocket(8800); ds.receive(dpre); //接收数据报,存放在指定的DatagramPacket对象中 //显示接收到的信息 String reply=new String(dpre.getData(),0,dpre.getLength()); System.out.println(dpre.getAddress().getHostAddress()+"说:"+reply); }catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ //关闭DatagramSocket对象 ds.close(); } } }
package cn.yu.SocketDemo; import java.io.IOException; import java.net.*; public class Receive { public static void main(String[] args) { DatagramPacket dp=null; DatagramSocket ds=null; DatagramPacket dpto=null; try{ //创建DatagramPacket对象,用来准备接收数据包 byte[] buf=new byte[1024]; dp=new DatagramPacket(buf,buf.length); //创建DatagramSocket对象,接收数据 ds=new DatagramSocket(8800); ds.receive(dp); //显示接收到的信息 String mess=new String(dp.getData(),0,dp.getLength()); System.out.println(dp.getAddress().getHostAddress()+"说:"+mess); String reply="你好,我在,请咨询!"; //显示与本地对话框 System.out.println("我 说:"+reply); //创建DatagramPacket对象,封装数据 SocketAddress sa=dp.getSocketAddress(); dpto=new DatagramPacket(reply.getBytes(),reply.getBytes().length ,sa); ds.send(dpto); }catch (IOException e) { e.printStackTrace(); }finally{ ds.close(); } } }
五、两种通信方式比较