JAVA网络编程之TCP通讯的实现步骤

深藏阁楼爱情的钟 2022-05-27 05:25 270阅读 0赞

与UDP相比,TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。

1、JAVA Socket简介

所谓socket 通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

以J2SDK-1.3为例,Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。具体JDK见: http://www.javaweb.cc/help/JavaAPI1.6/index.html?java/nio/ReadOnlyBufferException.html

重要的Socket API:

java.net.Socket继承于java.lang.Object,有八个构造器,其方法并不多,下面介绍使用最频繁的三个方法,其它方法大家可以见JDK-1.3文档。

. Accept方法用于产生”阻塞”,直到接受到一个连接,并且返回一个客户端的Socket对象实例。”阻塞”是一个术语,它使程序运行暂时”停留”在这个地方,直到一个会话产生,然后程序继续;通常”阻塞”是由循环产生的。

. getInputStream方法获得网络连接输入,同时返回一个InputStream对象实例。
. getOutputStream方法连接的另一端将得到输入,同时返回一个OutputStream对象实例。

注意:其中getInputStream和getOutputStream方法均会产生一个IOException,它必须被捕获,因为它们返回的流对象,通常都会被另一个流对象使用。

2、 ServerSocket

a、构造方法

具体API见:http://www.javaweb.cc/help/JavaAPI1.6/index.html?java/nio/ReadOnlyBufferException.html

ServerSocket() ~创建非绑定服务器套接字。

ServerSocket(int port) ~创建绑定到特定端口的服务器套接字。

ServerSocket(int port, int backlog) ~利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。

ServerSocket(int port, int backlog, InetAddress bindAddr) ~使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。

b、绑定端口

除了第一个不带参数的构造方法以外, 其他构造方法都会使服务器与特定端口绑定, 该端口有参数 port 指定. 例如, 以下代码创建了一个与 80 端口绑定的服务器:

  1. ServerSocket serverSocket = new ServerSocket(80);

如果运行时无法绑定到 80 端口, 以上代码会抛出 IOException, 更确切地说, 是抛出 BindException, 它是 IOException 的子类. BindException 一般是由以下原因造成的:

1)端口已经被其他服务器进程占用;

2)在某些操作系统中, 如果没有以超级用户的身份来运行服务器程序, 那么操作系统不允许服务器绑定到 1-1023之间的端口.

如果把参数 port 设为 0, 表示由操作系统来为服务器分配一个任意可用的端口. 有操作系统分配的端口也称为匿名端口. 对于多数服务器, 会使用明确的端口, 而不会使用匿名端口, 因为客户程序需要事先知道服务器的端口, 才能方便地访问服务器.

c、设定客户连接请求队列的长度

当服务器进程运行时, 可能会同时监听到多个客户的连接请求. 例如, 每当一个客户进程执行以下代码:

  1. Socket socket = new Socket("www.javathinker.org", 80);

就意味着在远程 www.javathinker.org 主机的 80 端口上, 监听到了一个客户的连接请求. 管理客户连接请求的任务是由操作系统来完成的. 操作系统把这些连接请求存储在一个先进先出的队列中. 许多操作系统限定了队列的最大长度, 一般为 50 . 当队列中的连接请求达到了队列的最大容量时, 服务器进程所在的主机会拒绝新的连接请求. 只有当服务器进程通过 ServerSocket 的 accept() 方法从队列中取出连接请求, 使队列腾出空位时, 队列才能继续加入新的连接请求.

对于客户进程, 如果它发出的连接请求被加入到服务器的请求连接队列中, 就意味着客户与服务器的连接建立成功, 客户进程从 Socket 构造方法中正常返回. 如果客户进程发出的连接请求被服务器拒绝, Socket 构造方法就会抛出 ConnectionException.

Tips: 创建绑定端口的服务器进程后, 当客户进程的 Socket构造方法返回成功, 表示客户进程的连接请求被加入到服务器进程的请求连接队列中. 虽然客户端成功返回 Socket对象, 但是还没跟服务器进程形成一条通信线路. 必须在服务器进程通过 ServerSocket 的 accept() 方法从请求连接队列中取出连接请求, 并返回一个Socket 对象后, 服务器进程这个Socket 对象才与客户端的 Socket 对象形成一条通信线路.

ServerSocket 构造方法的 backlog 参数用来显式设置连接请求队列的长度, 它将覆盖操作系统限定的队列的最大长度. 值得注意的是, 在以下几种情况中, 仍然会采用操作系统限定的队列的最大长度:

1)backlog 参数的值大于操作系统限定的队列的最大长度;

2)backlog 参数的值小于或等于0;

3)在ServerSocket 构造方法中没有设置 backlog 参数.

以下的 Client.java 和 Server.java 用来演示服务器的连接请求队列的特性.

客户端:

  1. import java.net.Socket;
  2. public class Client {
  3. public static void main(String[] args) throws Exception{
  4. final int length = 100;
  5. String host = "localhost";
  6. int port = 1122;
  7. Socket[] socket = new Socket[length];
  8. for(int i = 0;i<length;i++){
  9. socket[i] = new Socket(host,port);
  10. System.out.println("第"+(i+1)+"次连接成功!");
  11. }
  12. Thread.sleep(3000);
  13. for(int i=0;i<length;i++){
  14. socket[i].close();
  15. }
  16. }
  17. }

服务器端:

  1. import java.io.IOException;
  2. import java.net.ServerSocket;
  3. import java.net.Socket;
  4. public class Server {
  5. private int port = 1122;
  6. private ServerSocket serverSocket;
  7. public Server() throws Exception{
  8. serverSocket = new ServerSocket(port,3);
  9. System.out.println("服务器启动!");
  10. }
  11. public void service(){
  12. while(true){
  13. Socket socket = null;
  14. try {
  15. socket = serverSocket.accept();
  16. System.out.println("New connection accepted "+
  17. socket.getInetAddress()+":"+socket.getPort());
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. }finally{
  21. if(socket!=null){
  22. try {
  23. socket.close();
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. }
  30. }
  31. public static void main(String[] args) throws Exception{
  32. Server server = new Server();
  33. Thread.sleep(60000*10);
  34. server.service();
  35. }
  36. }

Client 试图与 Server 进行 100 次连接. 在 Server 类中, 把连接请求队列的长度设为 3. 这意味着当队列中有了 3 个连接请求时,

如果Client 再请求连接, 就会被 Server 拒绝. 下面按照以下步骤运行 Server 和 Client 程序.

1) 在Server 中只创建一个 ServerSocket 对象, 在构造方法中指定监听的端口为1122 和 连接请求队列的长度为 3 . 构造 Server 对象后, Server 程序睡眠 10 分钟, 并且在 Server 中不执行 serverSocket.accept() 方法. 这意味着队列中的连接请求永远不会被取出. 运行Server 程序和 Client 程序后, Client程序的打印结果如下:
第 1 次连接成功
第 2 次连接成功
第 3 次连接成功
Exception in thread “main” java.net.ConnectException: Connection refused: connect
…………….

从以上打印的结果可以看出, Client 与 Server 在成功地建立了3 个连接后, 就无法再创建其余的连接了, 因为服务器的队已经满了.

2)在Server中构造一个跟 1)相同的 ServerSocket对象, Server程序不睡眠, 在一个 while 循环中不断执行 serverSocket.accept()方法, 该方法从队列中取出连接请求, 使得队列能及时腾出空位, 以容纳新的连接请求. Client 程序的打印结果如下:
第 1 次连接成功
第 2 次连接成功
第 3 次连接成功
………..
第 100 次连接成功
从以上打印结果可以看出, 此时 Client 能顺利与 Server 建立 100 次连接.(每次while的循环要够快才行, 如果太慢, 从队列取连接请求的速度比放连接请求的速度慢的话, 不一定都能成功连接)

d、设定绑定的IP 地址

如果主机只有一个IP 地址, 那么默认情况下, 服务器程序就与该IP 地址绑定. ServerSocket 的第 4 个构造方法 ServerSocket(int port, int backlog, InetAddress bingAddr) 有一个 bindAddr 参数, 它显式指定服务器要绑定的IP 地址, 该构造方法适用于具有多个IP 地址的主机. 假定一个主机有两个网卡, 一个网卡用于连接到 Internet, IP为 222.67.5.94, 还有一个网卡用于连接到本地局域网, IP 地址为 192.168.3.4. 如果服务器仅仅被本地局域网中的客户访问, 那么可以按如下方式创建 ServerSocket:

  1. ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName(“192.168.3.4”));

e、 默认构造方法的作用

ServerSocket 有一个不带参数的默认构造方法. 通过该方法创建的 ServerSocket 不与任何端口绑定, 接下来还需要通过 bind() 方法与特定端口绑定.

这个默认构造方法的用途是, 允许服务器在绑定到特定端口之前, 先设置ServerSocket 的一些选项. 因为一旦服务器与特定端口绑定, 有些选项就不能再改变了.比如:SO_REUSEADDR 选项

在以下代码中, 先把 ServerSocket 的 SO_REUSEADDR 选项设为 true, 然后再把它与 8000 端口绑定:

  1. ServerSocket serverSocket = new ServerSocket();
  2. serverSocket.setReuseAddress(true); //设置 ServerSocket 的选项
  3. serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定

如果把以上程序代码改为:

  1. ServerSocket serverSocket = new ServerSocket(8000);
  2. serverSocket.setReuseAddress(true);//设置 ServerSocket 的选项

那么 serverSocket.setReuseAddress(true) 方法就不起任何作用了, 因为 SO_REUSEADDR 选项必须在服务器绑定端口之前设置才有效.

f、多线程示例

客户端:

  1. /*
  2. * 客户端
  3. */
  4. public class Client {
  5. public static void main(String[] args) {
  6. try {
  7. //1.创建客户端Socket,指定服务器地址和端口
  8. Socket socket=new Socket("localhost", 8888);
  9. //2.获取输出流,向服务器端发送信息
  10. OutputStream os=socket.getOutputStream();//字节输出流
  11. PrintWriter pw=new PrintWriter(os);//将输出流包装为打印流
  12. pw.write("用户名:whf;密码:789");
  13. pw.flush();
  14. socket.shutdownOutput();//关闭输出流
  15. //3.获取输入流,并读取服务器端的响应信息
  16. InputStream is=socket.getInputStream();
  17. BufferedReader br=new BufferedReader(new InputStreamReader(is));
  18. String info=null;
  19. while((info=br.readLine())!=null){
  20. System.out.println("我是客户端,服务器说:"+info);
  21. }
  22. //4.关闭资源
  23. br.close();
  24. is.close();
  25. pw.close();
  26. os.close();
  27. socket.close();
  28. } catch (UnknownHostException e) {
  29. e.printStackTrace();
  30. } catch (IOException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }

服务器端:

  1. /*
  2. * 基于TCP协议的Socket通信,实现用户登陆
  3. * 服务器端
  4. */
  5. public class Server {
  6. public static void main(String[] args) {
  7. try {
  8. //1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
  9. ServerSocket serverSocket=new ServerSocket(8888);
  10. Socket socket=null;
  11. //记录客户端的数量
  12. int count=0;
  13. System.out.println("***服务器即将启动,等待客户端的连接***");
  14. //循环监听等待客户端的连接
  15. while(true){
  16. //调用accept()方法开始监听,等待客户端的连接
  17. socket=serverSocket.accept();
  18. //创建一个新的线程
  19. ServerThread serverThread=new ServerThread(socket);
  20. //启动线程
  21. serverThread.start();
  22. count++;//统计客户端的数量
  23. System.out.println("客户端的数量:"+count);
  24. InetAddress address=socket.getInetAddress();
  25. System.out.println("当前客户端的IP:"+address.getHostAddress());
  26. }
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }

服务器处理类:

  1. /*
  2. * 服务器线程处理类
  3. */
  4. public class ServerThread extends Thread {
  5. // 和本线程相关的Socket
  6. Socket socket = null;
  7. public ServerThread(Socket socket) {
  8. this.socket = socket;
  9. }
  10. //线程执行的操作,响应客户端的请求
  11. public void run(){
  12. InputStream is=null;
  13. InputStreamReader isr=null;
  14. BufferedReader br=null;
  15. OutputStream os=null;
  16. PrintWriter pw=null;
  17. try {
  18. //获取输入流,并读取客户端信息
  19. is = socket.getInputStream();
  20. isr = new InputStreamReader(is);
  21. br = new BufferedReader(isr);
  22. String info=null;
  23. while((info=br.readLine())!=null){//循环读取客户端的信息
  24. System.out.println("我是服务器,客户端说:"+info);
  25. }
  26. socket.shutdownInput();//关闭输入流
  27. //获取输出流,响应客户端的请求
  28. os = socket.getOutputStream();
  29. pw = new PrintWriter(os);
  30. pw.write("欢迎您!");
  31. pw.flush();//调用flush()方法将缓冲输出
  32. } catch (IOException e) {
  33. // TODO Auto-generated catch block
  34. e.printStackTrace();
  35. }finally{
  36. //关闭资源
  37. try {
  38. if(pw!=null)
  39. pw.close();
  40. if(os!=null)
  41. os.close();
  42. if(br!=null)
  43. br.close();
  44. if(isr!=null)
  45. isr.close();
  46. if(is!=null)
  47. is.close();
  48. if(socket!=null)
  49. socket.close();
  50. } catch (IOException e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. }
  55. }

完!!!

发表评论

表情:
评论列表 (有 0 条评论,270人围观)

还没有评论,来说两句吧...

相关阅读