- 在现实网络传输应用中,
通常使用TCP
、IP
或UDP
这3种协议实现数据传输
。
在传输数据
的过程中,
需要通过一个双向的通信连接
实现数据的交互
。
在这个传输过程
中,
通常将这个双向链路的一端
称为Socket
,
一个Socket
通常由一个IP地址
和一个端口号
来确定。
在整个数据传输过程中
,Socket
的作用是巨大的。
在Java编程应用中,Socket
是Java网络编程的核心。
Socket基础
- 在
网络编程
中有两个主要的问题
,
一个是如何准确地定位
网络上一台或多台主机,
另一个就是找到主机后
如何可靠高效地
进行数据传输
。
在TCP/IP协议
中IP层
主要负责网络主机的定位
,数据传输
的路由
,
由IP地址
可以唯一地确定
Internet上的一台主机
。TCP层
则
提供面向应用的可靠(TCP)
的
或非可靠(UDP)
的数据传输机制
,
这是网络编程
的主要对象
,
一般不需要关心IP 层
是如何处理数据
的。
目前较为流行的网络编程模型
是客户机/服务器(C/S)结构
。
即通信双方
,一方作为服务器
等待(另一方作为的)客户
提出请求
并予以响应
。
客户则在需要服务时
向服务器提出申请
。
服务器一般作为守护进程
始终
运行,
监听网络端口,
一旦有客户请求,就会启动一个服务进程
来响应该客户,
同时自己继续监听服务端口
,
使后来的客户
也能及时得到服务
。
TCP/IP协议基础
TCP/IP
是Transmission Control Protocol/Internet Protocol
的简写,
中译名为传输控制协议
/因特网协议
,
又名网络通信协议
,
是Internet最基本的协议
、Internet国际互联网络
的基础
,
由网络层
的IP协议
和传输层
的TCP协议
组成。TCP/IP
定义了电子设备
如何连入因特网
,
以及数据
如何在它们之间传输的标准
。TCP/IP协议
采用了4层
的层级结构
,每一层
都呼叫它的下一层
所提供的协议
来完成自己的需求
。
也就是说,TCP
负责发现传输
的问题,
一旦发现问题
便发出信号
要求重新传输
,
直到所有数据
安全正确地传输到目的地
。
而IP
的功能是给因特网的每一台电脑规定
一个地址
。TCP/IP协议
不是TCP
和IP
这两个协议的合称
,
而是指因特网整个TCP/IP协议簇
。
从协议分层模型
方面来讲,TCP/IP
由4个层次
组成,
分别是网络接口层
、网络层
、传输层
、应用层
。其实
TCP/IP
协议并不完全符合OSI(Open System Interconnect)
的7层参考模型
,OSI
是传统的开放式系统互连参考模型
,
是一种通信协议
的7层抽象
的参考模型
,
其中每一层
执行某一特定任务
。
该模型的目的
是
使各种硬件
在相同的层次
上相互通信
。
这7层
是物理层、数据链路层(网络接口层)
、网络层(网络层)
、传送层(传输层)
、会话层、表示层和应用层(应用层)
。
而TCP/IP协议
采用了4层
的层级结构,每一层
都呼叫它的下一层
所提供的网络
来完成自己的需求
。
由于ARPANET
的设计者注重的是网络互联
,
允许通信子网(网络接口层)
采用已有的
或是将来有的各种协议
,
所以这个层次中没有提供专门的协议
。
实际上,TCP/IP协议
可以通过网络接口层
连接到任何网络
上,
例如X.25交换网
或IEEE802局域网
。
UDP协议
UDP
是User Datagram Protocol
的简称,
是一种无连接
的协议,
每个数据报
都是一个独立的信息
,
包括完整
的源地址
或目的地址
,
它在网络上以任何可能的路径
传往目的地
,
因此能否到达目的地
,到达
目的地的时间
以及内容的正确性
都是不能被保证
的。
在现实网络数据传输过程中
,大多数
功能是由TCP协议
和UDP协议
实现。(1)TCP协议
面向连接
的协议,
在Socket
之间进行数据传输
之前必然要建立连接
,
所以在TCP
中需要连接时间
。
TCP传输数据大小限制
,
一旦连接建立
起来,
双方的Socket
就可以按统一的格式
传输大的数据
。
TCP是一个可靠的协议
,
它确保接收方完全正确地
获取发送方
所发送的全部数据
。(2)UDP协议
每个数据报
中都给出了完整
的地址信息
,
因此无需要建立发送方
和接收方
的连接
。
UDP传输数据时是有大小限制
的,
每个被传输的数据报
必须限定在64KB
之内。
UDP是一个不可靠
的协议,发送方
所发送的数据报
并不一定以相同的次序
到达接收方
。
TCP、UDP选择的决定因素
(1)TCP在
网络通信
上有极强
的生命力
,
例如远程连接(Telnet)
和文件传输(FTP)
都需要不定长度
的数据
被可靠地传输
。
但是可靠的传输
是要付出代价的,
对数据内容正确性的检验
必然占用计算机的处理时间
和网络的带宽
,
因此TCP传输
的效率不如UDP高
。(2)UDP
操作简单
,而且仅需要较少的监护
,
因此通常用于局域网高可靠性
的分散系统
中Client/Server 应用程序
。
例如视频会议系统
,
并不要求音频视频数据
绝对的正确
,
只要保证连贯性
就可以了,
这种情况下显然使用UDP
会更合理
一些,
因为TCP
和UDP
都能达到这个保证连贯性
的门槛,
但是TCP
却要多占用更多的计算机资源
,杀鸡焉用牛刀
呢,
所有这种情况不用TCP
,用UDP
。
基于Socket的Java网络编程
网络上的两个程序通过一个
双向
的通信连接
实现数据的交换
,
这个双向链路
的一端
称为一个Socket
。Socket
通常用来实现客户方
和服务方
的连接。Socket
是TCP/IP协议
的一个十分流行
的编程方式,一个Socket
由一个IP地址
和一个端口号
唯一确定
。
但是,Socket
所支持的协议种类
也不光TCP/IP
一种,
因此两者之间是没有必然联系
的。
在Java环境
下,Socket编程
主要是指基于TCP/IP协议
的网络编程
。
1.Socket通信的过程
Server
端Listen(监听)
某个端口
是否有连接请求
,Client端
向Server 端
发出Connect(连接)请求
,Server端
向Client端
发回Accept(接收)消息
,
一个连接就建立起来了。Server端
和Client端
都可以通过Send
、Write
等方法与对方通信
。在
Java网络编程应用
中,
对于一个功能齐全的Socket
来说,
其工作过程包含如下所示的基本步骤。
(1)创建ServerSocket
和Socket
;
(2)打开连接到Socket
的输入/输出流
;
(3)按照一定的协议对Socket
进行读/写操作
;
(4)关闭IO流
和Socket
。
2.创建Socket
在
Java网络编程应用
中,
包java.net
中提供了两个类Socket
和ServerSocket
,
分别用来表示双向连接
的客户端
和服务端
。
这是两个封装得非常好的类,
其中包含了如下所示的构造方法
:Socket(InetAddress address, int port);
Socket(InetAddress address, int port, boolean stream);
Socket(String host, int prot);
Socket(String host, int prot, boolean stream);
Socket(SocketImpl impl);
Socket(String host, int port, InetAddress localAddr, int localPort);
Socket(InetAddress address, int port, InetAddress localAddr, int localPort);
ServerSocket(int port);
ServerSocket(int port, int backlog);
ServerSocket(int port, int backlog, InetAddress bindAddr)
在上述
构造方法
中,
参数address
、host
和port
分别是双向连接
中另一方
的IP地址
、主机名
和端口号
,stream
指明Socket
是流Socket
还是数据报Socket
,localPort
表示本地主机
的端口号
,localAddr
和bindAddr
是本地机器的地址
(ServerSocket
的主机地址
),impl
是Socket
的父类
,
既可以用来创建ServerSocket
又可以用来创建Socket
。
例如:
Socket client = new Socket("127.0.0.1", 80);
ServerSocket server = new ServerSocket(80);
- 注意:
必须小心地选择端口
,每一个端口
提供一种特定的服务
,
只有给出正确的端口
,才能获得相应的服务
。0~1023
的端口号
为系统所保留
,
例如HTTP
服务的端口号为80
,Telnet
服务的端口号为21
,FTP
服务的端口号为23
,
所以我们在选择端口号时
,最好选择一个大于1023
的数
以防止发生冲突
。
另外,
在创建Socket
时如果发生错误
,将产生IOException
,
在程序中
必须对之做出处理
。
所以在创建Socket
或ServerSocket
时必须捕获
或抛出异常
。
TCP编程详解
TCP/IP通信协议
是一种可靠
的网络协议
,
能够在通信的两端
各建立一个Socket
,
从而在通信的两端
之间形成网络虚拟链路
。
一旦建立了虚拟的网络链路
,两端的程序
就可以通过虚拟链路
进行通信
。
Java语言对TCP网络通信
提供了良好的封装
,
通过Socket对象
代表两端
的通信端口
,
并通过Socket
产生的IO流
进行网络通信
。
这里先笔记Java应用
中TCP编程的基本知识,
为后面的Android编程
打下基础。
使用ServerSocket
在Java程序中,
使用
类ServerSocket
接受其他通信实体
的连接请求
。对象ServerSocket
的功能是监听
来自客户端的Socket连接
,
如果没有连接
则会一直处于等待状态
。在类
ServerSocket
中包含了如下监听客户端连接请求的方法:Socket accept()
:如果接收到一个客户端Socket
的连接请求
,
该方法将返回
一个与客户端Socket
对应的Socket
,
否则该方法
将一直处于等待状态
,线程也被阻塞
。-
为了创建
ServerSocket对象
,ServerSocket类
为我们提供了如下构造器
:ServerSocket(int port)
:
用指定的端口port
创建一个ServerSocket
,
该端口
应该是有一个有效
的端口整数值0~65535
。ServerSocket(int port,int backlog)
:
增加一个用来改变连接队列长度
的参数backlog
。ServerSocket(int port,int backlog,InetAddress localAddr)
:
在机器(服务器、本机等)
存在多个IP地址
的情况下,
允许通过localAddr
这个参数
来指定将ServerSocket
绑定到指定的IP地址
。
当使用
ServerSocket
后,
需要使用ServerSocket
中的方法close()
关闭该ServerSocket
。在通常情况下,
因为服务器不会只接受
一个客户端请求
,
而是会不断地接受
来自客户端
的所有请求
,
所以可以通过循环
来不断
地调用ServerSocket
中的方法accept()
。例如下面的代码。
//创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket ss = new ServerSocket(30000);
//采用循环不断接受来自客户端的请求
while (true)
{
//每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket s = ss.accept();
//下面就可以使用Socket进行通信了
...
}
- 在上述代码中,
创建的ServerSocket
没有指定IP地址
,
该ServerSocket
会绑定
到本机默认
的IP地址
。
在代码中使用30000
作为该ServerSocket
的端口号
,
通常推荐使用10000
以上的端口
,
主要是为了避免与其他应用程序
的通用端口
冲突
。
使用Socket
-
在客户端可以使用
Socket
的构造器
实现``和指定服务器
的连接
,
在Socket
中可以使用如下两个构造器:Socket(InetAddress/String remoteAddress, int port)
:
创建连接到指定远程主机
、远程端口的Socket
,
该构造器没有指定本地地址
、本地端口
,本地IP地址
和端口
使用默认值
。Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort)
:
创建连接到指定远程主机
、远程端口
的Socket
,
并指定本地IP地址
和本地端口号
,
适用于本地主机
有多个IP地址
的情形。
在使用上述
构造器
指定远程主机
时,
既可使用InetAddress
来指定,也可以使用String对象
指定,
在Java
中通常使用String对象
指定远程IP
,例如192.168.2.23
。
当本地主机只有一个IP地址时,建议使用第一个方法,简单方便。
例如下面的代码:
//创建连接到本机、30000端口的Socket
Socket s = new Socket("127.0.0.1" , 30000);
当程序执行
上述代码
后会连接到指定服务器
,
让服务器端
的ServerSocket
的方法accept()
向下执行,
于是服务器端
和客户端
就产生一对互相连接的Socket
。
上述代码连接到“远程主机”的IP地址是127.0.0.1
,
此IP地址
总是代表本机的IP地址
。
这里例程的服务器端
、客户端
都是在本机
运行,
所以Socket
连接到远程主机
的IP地址
使用127.0.0.1。当
客户端
、服务器端
产生对应的Socket
之后,
程序无须再区分服务器端和客户端,
而是通过各自的Socket进行通信。-
在
Socket
中提供如下两个方法获取输入流
和输出流
:InputStream getInputStream()
:
返回该Socket对象
对应的输入流
,
让程序通过该输入流
从Socket中取出数据。OutputStream getOutputStream()
:
返回该Socket对象
对应的输出流
,
让程序通过该输出流
向Socket
中输出数据
。
TCP协议的服务器端例程:
public class Server
{
public static void main(String[] args)
throws IOException
{
//创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket myss = new ServerSocket(30001);
//采用循环不断接受来自客户端的请求
while (true)
{
//每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket s = myss.accept();
//将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(s.getOutputStream());
//进行普通IO操作
ps.println("凌川江雪!");
ps.println("望川霄云!");
ps.println("万年太久,只争朝夕!");
ps.println("人间正道是沧桑!");
ps.println("穷善其身,达济天下!");
//关闭输出流,关闭Socket
ps.close();
s.close();
}
}
}
- 上述代码建立了
ServerSocket监听
,
并且使用Socke
t获取了输出流
,
执行后不会显示任何信息。 - 对应的TCP协议的客户端例程:
public class Client {
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1" , 30001);
//将Socket对应的输入流包装成BufferedReader
BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
//进行普通IO操作
StringBuilder response = new StringBuilder();
String line;
//一行一行地读取并加进stringbuilder
while((line = br.readLine()) != null){
response.append(line + "\n");
}
System.out.println("来自服务器的数据:" + "\n" + response.toString());
//关闭输入流、Socket
br.close();
socket.close();
}
}
-
上述代码使用Socket建立了与指定IP、指定端口的连接,
并使用Socket获取输入流读取数据,
之后处理一下数据然后打印在工作台。先
运行服务端Class
,再
运行客户端Class
,运行结果: 由此可见,
一旦使用ServerSocket
和Socket
建立网络连接
之后,
程序通过网络通信
与普通IO
并没有太大的区别。
如果先运行上面程序中的Server
类,
将看到服务器一直处于等待状态
,
因为服务器使用了死循环
来接受来自客户端
的请求;
再运行Client
类,
将可看到程序输出“来自服务器的数据:...!”,
这表明客户端和服务器端通信成功。
TCP中的多线程
刚刚实操的例程中,
Server
和Client
只是进行了简单的通信操作,
当服务器接收到客户端连接之后,服务器向客户端输出一个字符串,
而客户端
也只是读取
服务器的字符串后
就退出
了。在实际应用中,
客户端
可能需要和服务器端
保持长时间通信
,
即服务器
需要不断
地读取客户端数据
,
并向客户端写入
数据,客户端
也需要不断
地读取
服务器数据,
并向服务器写入
数据。当使用
readLine()
方法读取数据
时,
如果在该方法成功返回之前线程
被阻塞
,则程序无法继续执行
。
所以服务器
很有必要为每个Socket
单独启动一条线程
,每条线程
负责与一个客户端
进行通信
。另外,
因为客户端
读取服务器数据
的线程
同样会被阻塞
,
所以系统
应该单独
启动一条线程
,该组线程
专门负责读取服务器数据
。假设要开发一个
聊天室程序
,
在服务器端
应该包含多条线程
,
其中每个Socket对应一条线程
,
该线程
负责读取 Socket 对应输入流
的数据
(从客户端
发送过来的数据
),
并将读到的数据
向每个Socket输出流
发送一遍
(将一个客户端
发送的数据
“广播”
给其他客户端
);因此需要在
服务器端
使用List
来保存所有的Socket
。
在具体实现
时,
为服务器
提供了如下两个类
:
创建ServerSocket监听
的主类
。
处理每个Socket通信
的线程类
。
1/4 接下来介绍具体实现流程,首先看下面的IServer
Class:
public class IServer
{
//定义保存所有Socket的ArrayList
public static ArrayList<Socket> socketList = new ArrayList<Socket>();
public static void main(String[] args)
throws IOException
{
ServerSocket ss = new ServerSocket(30000);
while(true)
{
//此行代码会阻塞,将一直等待别人的连接
Socket s = ss.accept();
socketList.add(s);
//每当客户端连接后启动一条ServerThread线程为该客户端服务
new Thread(new Serverxian(s)).start();
}
}
}
IServer
类中,服务器端(ServerSocket )
只负责接受客户端Socket
的连接请求
,
每当客户端Socket
连接到该ServerSocket
之后,
程序将客户端对应的Socket(客户Socket的对面一端)
加入socketList集合
中保存
,
并为该Socket
启动一条线程
(Serverxian
),
该线程
负责处理 该Socket所有 的 通信任务
。
小结:IServer
类完成的业务是:
1.接收客户端Socket
,
2.保存对应返回的Socket
,
3.启动处理线程
。
2/4 接着看服务器端线程类文件:
package liao.server;
import java.io.*;
import java.net.*;
import java.util.*;
//负责处理每个线程通信的线程类
public class Serverxian implements Runnable
{
//定义当前线程所处理的Socket
Socket s = null;
//该线程所处理的Socket所对应的输入流读取器
BufferedReader br = null;
public Serverxian(Socket s)
throws IOException
{
this.s = s;
//初始化该Socket对应的输入流
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
}
public void run()
{
try
{
String content = null;
//采用循环不断从Socket中读取客户端发送过来的数据
while ((content = readFromClient()) != null)
{
//遍历socketList中的每个Socket,
//将读到的内容向每个Socket发送一次
for (Socket s : IServer.socketList)
{
//将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println(content);
}
}
}
catch (IOException e)
{
//e.printStackTrace();
}
}
//定义读取客户端数据的方法
private String readFromClient()
{
try
{
return br.readLine();
}
//如果捕捉到异常,表明该Socket对应的客户端已经关闭
catch (IOException e)
{
//删除该Socket。
IServer.socketList.remove(s);
}
return null;
}
}
Serverxian类(服务器端线程类)中,
注意是线程类,继承Runnable,重写run方法
会不断读取客户端数据,
在获取时使用方法readFromClient()来读取客户端数据。
如果读取数据过程中捕获到 IOException异常,
则说明此Socket对应的客户端Socket出现了问题,
程序就会将此Socket从socketList中删除。
当服务器线程读到客户端数据之后会遍历整个socketList集合,
并将该数据向socketList集合中的每个Socket发送一次,
该服务器线程将把从Socket中读到的数据
向socketList中的每个Socket转发一次。
上述代码能够不断获取
Socket
输入流中的内容,
当获取Socket输入流
中的内容
后,
直接将这些内容
打印在控制台
。
先运行上面程序中的类IServer
,
该类运行后作为本应用的服务器,不会看到任何输出。接着可以运行多个 IClient——相当于启动多个聊天室客户端登录该服务器,此时在任何一个客户端通过键盘输入一些内容后单击“回车”键,将可看到所有客户端(包括自己)都会在控制台收到刚刚输入的内容,这就简单实现了一个聊天室的功能。-
运行结果如下动图所示:
(这个链接是
在Eclipse上,同时运行多个java程序,
用不同的console显示运行信息的方法)
同时启动两个客户端,
来回切换客户端进行“聊天”,
客户端由于服务端的socket传输,
可以相互收到彼此的信息;