一、通信前提
两台计算机要进行通信,就要满足以下条件:
1.两台主机要有唯一的标识,也就是IP地址,用来表示它们所处的身份和位置。
2.要有共同的语言,否则它们无法交流,所以就需要协议来帮忙。
3.每台主机要有相应的端口号。一台主机可以辨别多个应用程序是依靠端口号来进行辨别的。我是用qq发送的信息是不会被对方的MSA接收到,因为有端口号的存在。
二、Java对Socket提供的支持
Java针对网络通信的不同层次,提供了不同的网络支持
InetAddress类
InetAddress用于表示互联网协议(IP)地址。InetAddress没有构造方法,也就是没办法通过new来创建一个InetAddress,但是它提供了一些静态的方法,并且这些静态的方法返回一个InetAddress的实例,所以可以通过它提供的静态方法获取实例。
- 构造方法
InetAddress getByAddress(byte[] addr):根据原始IP地址返回InetAddress对象
InetAddress getByAddress(String host,byte[] addr):根据提供的主机名和IP地址创建InetAddress对象
InetAddress getByName():根据主机名获取InetAddress实例
InetAddress getLocalHost():获取本机InetAddress实例
- 通用方法
String getHostAddress():返回IP地址,如1.1.1.0
String getHostName():获取此IP地址的主机名
int hashCode():返回此IP地址的哈希码
URL类
URL为统一资源定位符。
- 构造方法
URL(String spec):根据String表示形式创建URL对象
URL(String protocol,String host,int port,String file):根据指定的protocol、host、port号和file创建URL对象
URL(String protocol,String host,int port,String file,URLStreamHandler handler):根据指定的protocol、host、port号、file和hadler创建URL对象
URL(String protocol,String host,String file):根据指定的protocol、host和file创建URL对象
URL(URL context,String spec):通过在指定的上下文中对给定的spec进行解析创建URL
URL(URL context,String spec,URLStreamHandler handler):通过在指定的上下文中用指定的处理程序对给定的spec进行解析创建URL
- 通用方法
getProtocol():得到协议,如HTTP
getHost():得到主机名,如www.abc.com
getPort():得到端口号,如8080
getPath():得到文件路径,如/index.html
getFile():得到文件名,如/index.html?userName=abc
getRef():得到相对路径,如test
getQuery():得到查询字符串,如userName=abc
我们还可以使用URL读取网页内容:通过URL对象的openStream()方法可以得到指定资源的输入流,通过输入流读取网页上的数据:
URL url = new URL("http://www.baidu.com");
InputStream is = url.openStream();
InputStreamReader isr = new InputStreamReader(is,"utf-8");
BufferReader br = new BufferReader(isr);
String data = br.readLine();
Socket类
Socket:Socket类实现了一个客户端socket,作为两台机器通信的终端,默认采用的传输层协议为TCP。Socket其实就是客户端的套接字(可以认为是IP地址+端口,用来区分实现不同应用程序的通信)。
- 构造方法
Socket():默认构造方法
Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定IP地址和指定端口号
Socket(InetAddress address,int port,InetAddress localAddr,int localPort):创建一个套接字并将其连接到指定远程地址上的指定远程端口
Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指定端口号
Socket(String host,int port,InetAddress localAddr,int localPort):创建一个套接字并将其连接到指定远程地址上的指定远程端口
注意,Socket通信的时,字符流字节流在发送或接收数据的时候,发送端和接收端的类型一定要一致(类型指的是:字节流或字符流)。比如:字节流发出去的必须要用字节流接收。
ServerSocket
ServerSocket类实现了一个服务器socket,一个服务器socket等待客户端网络请求,然后基于这些请求执行操作,并返回给请求者一个结果。
- 构造方法
ServerSocket()
ServerSocket(int port)
ServerSocket(int port,int backlog)
ServerSocket(int port,int backlog,InetAddress bindAddr)
- 通用方法
Socket accept():侦听并接受到此套接字的连接
void bind(SocketAddress endpoint):将ServerSocket绑定到特定地址(IP地址和端口号)
void bind(SocketAddress endpoint,int backlog):将ServerSocket绑定到特定地址(IP地址和端口号)
void close():关闭此套接字
ServerSocketChannel getChannel():返回与此套接字关联的唯一ServerSocketChannel对象(如果用)
InetAddress getInetAddress():返回此服务器套接字的本地地址
int getLocalPort():返回此套接字在其上侦听的端口
SocketAddress getLocalSocketAddress():返回此套接字绑定的端点的地址
※当我们调用accept()方法的时候,会阻塞当前的侦听,等待客户端的连接。当连接成功以后,会创建一个socket的实例,用来与当前的客户端进行通信。
DatagramSocket类
使用UDP协议,将数据保存在数据报中,通过网络进行通信
- 构造方法
DatagramSocket():创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、本机所有可用端口中随机选择的某个端口。
DatagramSocket(int prot):创建一个DatagramSocket实例,并将该对象绑定到本机默认IP地址、指定端口。
DatagramSocket(int port, InetAddress laddr):创建一个DatagramSocket实例,并将该对象绑定到指定IP地址、指定端口。
- 通用方法
receive(DatagramPacket p):从该DatagramSocket中接收数据报。
send(DatagramPacket p):以该DatagramSocket对象向外发送数据报。
DatagramPackage类
- 构造方法
DatagramPacket(byte[] buf,int length):以一个空数组来创建DatagramPacket对象,该对象的作用是接收DatagramSocket中的数据。
DatagramPacket(byte[] buf, int length, InetAddress addr, int port):以一个包含数据的数组来创建DatagramPacket对象,创建该DatagramPacket对象时还指定了IP地址和端口--这就决定了该数据报的目的地。
DatagramPacket(byte[] buf, int offset, int length):以一个空数组来创建DatagramPacket对象,并指定接收到的数据放入buf数组中时从offset开始,最多放length个字节。
三、实现基于TCP的Socket通信
基于TCP通信首先要建立连接。
3.1 实现通信的基本步骤
服务器端
①创建ServerSocket对象,绑定监听端口
②通过accept()方法监听客户端请求
③连接建立后,通过输入流读取客户端发送的请求信息
④通过输出流向客户端发送向响应信息
⑤关闭相关资源
客户端
①创建Socket对象,指明需要连接的服务器的地址和端口号
②连接建立后,通过输出流向服务器端发送请求信息
③通过输入流获取服务器响应的信息
④关闭相关资源
多线程服务器
①服务器端创建ServerSocket,循环调用accept()等待客户端连接
②客户端创建一个socket并请求和服务端连接
③服务器端接受客户端请求,创建socket与该客户建立专线连接
④建立连接的两个socket在一个单独的线程上对话
⑤服务器端继续等待新的连接
3.2 代码示例
服务端步骤:
public class TCPServer{
public static void main(String[] args){
try{
//创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = null;
//记录客户端数量
Integer count = 0;
System.out.println("*****服务器即将启动,等待客户端的连接*****");
//循环监听,等待客户端连接
while(true){
//调用accept方法开始监听,等待客户端连接
//一旦调用这个方法以后,它就会处于一个阻塞的状态等待客户端的侦听
Socket socket = serverSocket.accept();
//创建一个新的线程
ServerThread serverThread = new ServerThread(socket);
//可以使用serverThread.setPriority()设置线程优先级,范围为[1,10]
serverThread.start();
InetAddress address = socket.getInetAddress();
System.out.println("当前客户端的IP:"+address.getHostAddress());
count++;
System.out.println("客户端的数量为:"+count);
}catch(IOException e){
e.printStackTrack();
}
}
//服务器线程处理类
public class ServerThread extends Thread{
//和本线程相关的socket
Socket socket = null;
public ServerThread(Socket socket){
this.socket = socket;
}
//线程执行操作,响应客户端请求
public static void run(){
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
OurputStream os = null;
PrintWriter pw = null;
try{
//字节输入流
is = socket.getInputStream();
//提高读取性能,把字节流变成字符流
isr = new InputStreamReader(is);
//把输入流添加到缓冲中,以缓冲的方式进行读取
br = new BufferedReader(isr);
String info = null;
while((info=br.readLine())!=null){
System.out.println("我是服务器,客户端说:"+info);
}
socket.shutdownInput();
//获取输入流,响应客户端请求
os = socket.getOutputStream();//字节输出流
pw = new PrintWriter(os);
pw.write("欢迎你");
pw.flush();
}catch(IOException e){
e.printStackTrack();
}finally{
//关闭资源
try{
if(pw!=null)
pw.close();
if(os!=null)
os.close();
if(br!=null)
br.close();
if(isr!=null)
isr.close();
if(is!=null)
is.close();
if(socket!=null)
socket.close();
}catch(IOException e){
e.printStackTrack();
}
}
}
}
注意:对于同一个socket,如果关闭了输出流,则与该输出流关联的socket也会被关闭,所以一般不用关闭流,直接关闭socket即可
客户端步骤:
public class TCPClient{
public static void main(String[] args){
try{
//创建服务器端socket,指定服务器地址和端口(告诉socket服务器端在哪)
Socket socket = new Socket("localhost",8888);
//获取输入流,用来向服务器端发送信息
OurputStream os = socket.getOutputStream();//字节输出流
PrintWriter pw = new PrintWriter(os);
pw.write("用户名:tom;密码:456");
pw.flush();//刷新缓存
//关闭当前socket输出流
socket.shutdownOutput();
InputStream is = socket.getInputStream();//字节输入流
//提高读取性能,把字节流变成字符流
InputStreamReader isr = new InputStreamReader(is);
//把输入流添加到缓冲中,以缓冲的方式进行读取
BufferedReader br = new BufferedReader(isr);
String info = null;
while((info=br.readLine())!=null){
System.out.println("我是客户端,服务器说:"+info);
}
br.close();
isr.close();
is.close();
pw.close();
os.close();
socket.close();
serverSocket.close();
}catch(IOException e){
e.printStackTrack();
}
}
}
运行程序:
注意:服务端要优先于客户端启动,等待命令的传输。
一开始运行服务器端(此时还未运行客户端):
然后我们来运行客户端:
此时的服务器端:
然后客户端就会接收到服务器端的响应信息:
如果我们将客户端输出信息改一下,然后再启动多几个客户端向该服务器端发送请求,就会看到客户端数量不断增加。
3.3 TCP通信传输对象
实际的应用当中客户端向服务器端发送用户信息,更多的应该是以对象的形式去进行传输。我们把用户名、密码、用户ID等等,包装成对象,传递的是一个user对象,这个时候可以使用ObjectInputStream和ObjectOutputStream来实现对象的传输。
OutputStream os = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
User u = new User("root","root");
oos.writeObject(u);
我们之前在进行数据传递的时候,传递的是字符串,如果想实现文件的传递,也可以使用ObjectInputStream和ObjectOutputStream。
ObjectInputStream cis = null;
//下载后首先生成一个临时文件
String fileNameTemp = "test.txt";
String downloadPath = ConfigManager.getInstance().getString(Constants.CLIENT_DOWNLOAD_PATH);
try{
File fileTemp = new File(downloadPath+"/"+fileNameTemp);
if(fileTemp.exists()){
fileTemp.delete();
}
fileTemp.createNewFile();
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(fileTemp));
//接收服务器的文件
cis = new ObjectInputStream(socket.getInputStream());
byte[] buf = new byte[1024];
int len;
while((len=cis.read(buf))!=-1){
fos.write(buf,0,len);
fos.flush();
}
}
四、实现基于UDP的Socket通信
基于UTP的通信是通过发送数据包来传送信息。
4.1 相关操作类
DatagramPacket:表示数据报包
DatagramSocket:进行端到端通信的类
4.2 实现通信的基本步骤
服务器端
①创建DatagramSocket,指定端口号
②创建DatagramPacket,用来接收客户端发送的信息
③读取收客户端发送的数据信息
④读取数据
客户端
①定义发送信息
②创建DatagramPacket,包含将要发送的信息
③创建DatagramSocket
④发送数据
4.3 代码示例
服务端步骤:
public class UDPServer{
public static void main(String[] args){
try{
//创建一个服务器端,指定绑定的端口
DatagramSocket socket = new DatagramSocket(8888);
//创建数据报,用来接收客户端发送的信息
byte[] data = new byte[1024];//创建字节数组,指定接收数据包的大小
//把数据保存在data数组中
DatagramPacket packet = new DatagramPacket(data,data.length);
System.out.println("*****服务器即将启动,等待客户端的连接*****");
socket.receive(packet);
String info = new String(data,0,package.getLength());
System.out.println("我是服务器,客户端说:"+info);
/**
*向客户端响应数据
**/
//定义客户端地址、端口号和数据
InetAddress address = packet.getAddress();
Integer port = packet.getPort();
byte[] data2 = "欢迎你!".getBytes();
//创建数据报,包含响应的数据信息
DatagramPacket packet2 = new DatagramPacket(data2,data2.length,address,port);
//响应客户端
socket.send(packet2);
//关闭资源
socket.close();
}catch(IOException e){
e.printStackTrack();
}
}
客户端步骤:
public class UDPClient{
public static void main(String[] args){
/**
* 向服务器端发送数据
**/
try{
//定义服务器的地址、端口号和数据
InetAddress address = InetAddress.getByName("localhost");
int port = 8080;
byte[] data = "用户名:admin;密码:123".getBytes();
//创建数据报,包含响请求的数据信息
DatagramPacket packet = new DatagramPacket(data,data.length,address,port);
//创建DatagramSocket对象,实现数据发送
DatagramSocket serverSocket = new DatagramSocket();
socket.send(packet);
/**
* 接收服务器端相应的数据
**/
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2,data2.length);
socket.receive(packet2);
String reply = new String(data2,0,data2.getLength());
System.out.println("我是客户端,服务器说:"+reply);
socket.close();
}catch(IOException e){
e.printStackTrack();
}
}
五、QQ聊天使用TCP协议还是UDP?
首先我们来了解一下使用TCP和UDP通行的利弊。
TCP是面向连接的协议,即需要建立连接,其传输机制为可靠传输,TCP连接会大量占用网络资源,所以网络开销也会相对大,可能导致网络不畅。
UDP协议是无连接方式的协议,其传输机制为不可靠传输,优点是效率高、速度快、占用资源少。但是它不像TCP一样有包重传等机制,因此采用UDP协议的信息在传送过程中很容易丢失,因此这就需要辅助的算法实现包重传机制以保证信息不丢失。UDP可以穿透大部分代理服务器。
国内网络环境非常复杂,在这些复杂的情况下,客户端之间能彼此建立起来TCP连接的概率较小,而且很多用户采用的方式是通过代理服务器共享一条线路上网的方式,严重影响传送信息的效率。QQ的服务器设计容量是海量级的应用,一台服务器要同时容纳十几万的并发连接,因此服务器端只有采用UDP协议与客户端进行通讯才能保证这种超大规模的服务。如果用TCP协议,还要维护几亿连接,这是不现实。每个连接都要做状态维护,要开内存,要占用很多时间周期,划不来。
使用UDP进行交互通信的好处在于,延迟较短,对数据丢失的处理比较简单。同时,如果使用QQ语言和QQ视频的话,UDP的优势就更加突出:如果数据丢失,不会有重传。因为用户一般来说可以接收图像模糊一点、声音稍微不清晰一点,但是难以接受声音和画面不同步的情况。
采用UDP协议,通过服务器中转方式。但是服务器端在QQ的时候迫于负荷过重的压力并没有对主动发往客户端的信息进行传输控制,因此容易造成服务器中转信息丢失的现象。但是,作为聊天软件,怎么可以采用这样的不可靠方式来传输消息呢?于是,腾讯采用了上层协议来保证可靠传输:客户端使用UDP协议发出消息后,如果消息发送失败,客户端会提示消息发送失败,并可重新发送。
不过QQ并不是完全基于UDP实现,比如在使用QQ进行文件传输等活动的时候,就会使用P2P技术作为可靠传输的保证,不需要服务器中转。