一、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();
}
}
}
五、两种通信方式比较