2011-06-30 14 views
5

Tôi có một máy khách Java kết nối với máy chủ C++ bằng cách sử dụng TCP Sockets sử dụng Java NIO. Điều này làm việc dưới Linux, AIX và HP/UX nhưng dưới Solaris sự kiện OP_CONNECT không bao giờ cháy.Vấn đề Java Solaris NIO OP_CONNECT

Thông tin chi tiết:

  • Selector.select() được trở về 0, và 'chọn bộ chủ chốt' trống.
  • Sự cố chỉ xảy ra khi kết nối với máy cục bộ (qua giao diện loopback hoặc ethernet), nhưng hoạt động khi kết nối với máy từ xa.
  • Tôi đã xác nhận sự cố trong hai máy Solaris 10 khác nhau; một SPARC vật lý và x64 ảo (VMWare) sử dụng cả hai phiên bản JDK 1.6.0_21 và _26.

Dưới đây là một số mã kiểm tra mà chứng tỏ vấn đề:

import java.io.IOException; 
import java.net.InetSocketAddress; 
import java.nio.ByteBuffer; 
import java.nio.channels.SelectionKey; 
import java.nio.channels.Selector; 
import java.nio.channels.SocketChannel; 
import java.util.HashSet; 
import java.util.Iterator; 
import java.util.Set; 

public class NioTest3 
{ 
    public static void main(String[] args) 
    { 
     int i, tcount = 1, open = 0; 
     String[] addr = args[0].split(":"); 
     int port = Integer.parseInt(addr[1]); 
     if (args.length == 2) 
      tcount = Integer.parseInt(args[1]); 
     InetSocketAddress inetaddr = new InetSocketAddress(addr[0], port); 
     try 
     { 
      Selector selector = Selector.open(); 
      SocketChannel channel; 
      for (i = 0; i < tcount; i++) 
      { 
       channel = SocketChannel.open(); 
       channel.configureBlocking(false); 
       channel.register(selector, SelectionKey.OP_CONNECT); 
       channel.connect(inetaddr); 
      } 
      open = tcount; 
      while (open > 0) 
      { 
       int selected = selector.select(); 
       System.out.println("Selected=" + selected); 
       Iterator<SelectionKey> it = selector.selectedKeys().iterator(); 
       while (it.hasNext()) 
       { 
        SelectionKey key = it.next(); 
        it.remove(); 
        channel = (SocketChannel)key.channel(); 
        if (key.isConnectable()) 
        { 
         System.out.println("isConnectable"); 
         if (channel.finishConnect()) 
         { 
          System.out.println(formatAddr(channel) + " connected"); 
          key.interestOps(SelectionKey.OP_WRITE); 
         } 
        } 
        else if (key.isWritable()) 
        { 
         System.out.println(formatAddr(channel) + " isWritable"); 
         String message = formatAddr(channel) + " the quick brown fox jumps over the lazy dog"; 
         ByteBuffer buffer = ByteBuffer.wrap(message.getBytes()); 
         channel.write(buffer); 
         key.interestOps(SelectionKey.OP_READ); 
        } 
        else if (key.isReadable()) 
        { 
         System.out.println(formatAddr(channel) + " isReadable"); 
         ByteBuffer buffer = ByteBuffer.allocate(1024); 
         channel.read(buffer); 
         buffer.flip(); 
         byte[] bytes = new byte[buffer.remaining()]; 
         buffer.get(bytes); 
         String message = new String(bytes); 
         System.out.println(formatAddr(channel) + " read: '" + message + "'"); 
         channel.close(); 
         open--; 
        } 
       } 
      } 

     } 
     catch (IOException e) 
     { 
      e.printStackTrace(); 
     } 
    } 

    static String formatAddr(SocketChannel channel) 
    { 
     return Integer.toString(channel.socket().getLocalPort()); 
    } 
} 

Bạn có thể chạy này bằng cách sử dụng dòng lệnh:

java -cp . NioTest3 <ipaddr>:<port> <num-connections> 

đâu cổng nên là 7 nếu bạn đang chạy chống lại một dịch vụ tiếng vọng thực; tức là:

java -cp . NioTest3 127.0.0.1:7 5 

Nếu bạn không thể nhận được dịch vụ echo thực thì nguồn đó là here. Biên dịch máy chủ vang dưới Solaris với:

$ cc -o echoserver echoserver.c -lsocket -lnsl 

và chạy nó như thế này:

$ ./echoserver 8007 > out 2>&1 & 

này đã được báo cáo với Mặt Trời như một bug.

Trả lời

1

Tôi đã làm việc xung quanh lỗi này bằng cách sử dụng sau đây:

Nếu Selector.select() trả về 0 (và thời gian chờ không, nếu phiên bản timeout được sử dụng) sau đó:

  1. lặp qua các phím đăng ký với công cụ chọn qua selector.keys().iterator() (ghi nhớ không phải để gọi iterator.remove()).
  2. Nếu OP_CONNECT quyền lợi đã được đặt bằng khóa thì hãy gọi channel.finishConnect() và thực hiện bất kỳ việc gì đã được thực hiện nếu isConnectable() đã trả lại true.

Ví dụ:

if (selected == 0 && elapsed < timeout) 
{ 
    keyIter = selector.keys().iterator(); 
    while (keyIter.hasNext()) 
    { 
     key = keyIter.next(); 
     if (key.isValid()) 
     { 
      channel = (SocketChannel)key.channel(); 
      if (channel != null) 
      { 
       if ((key.interestOps() & SelectionKey.OP_CONNECT) != 0) 
       { 
        if (channel.finishConnect()) 
        { 
         key.interestOps(0); 
        } 
       } 
      } 
     } 
    } 
}   

này đã được báo cáo với Mặt Trời như một bug.

9

Báo cáo lỗi của bạn đã bị đóng dưới dạng 'không phải là lỗi', với giải thích. Bạn đang bỏ qua kết quả của connect(), nếu đúng nghĩa là OP_CONNECT sẽ không bao giờ kích hoạt vì kênh đã được kết nối. Bạn chỉ cần toàn bộ OP_CONNECT/finishConnect() megillah nếu nó trả về false. Vì vậy, bạn nên thậm chí không đăng ký OP_CONNECT trừ connect() lợi nhuận sai, hãy để một mình đăng ký nó trước khi bạn thậm chí gọi connect().

nhận xét thêm:

Dưới mui xe, OP_CONNECT và OP_WRITE là những điều tương tự, điều này giải thích một phần của nó.

Vì bạn có một chuỗi duy nhất cho việc này, giải pháp thay thế sẽ là thực hiện kết nối ở chế độ chặn, sau đó chuyển sang không chặn cho I/O.

Bạn có đang chọn() sau khi đăng ký kênh bằng Công cụ chọn không?

Cách đúng xử lý non-blocking kết nối như sau:

channel.configureBlocking(false); 
if (!channel.connect(...)) 
{ 
    channel.register(sel, SelectionKey.OP_CONNECT, ...); // ... is the attachment, or absent 
} 
// else channel is connected, maybe register for OP_READ ... 
// select() loop runs ... 
// Process the ready keys ... 
if (key.isConnectable()) 
{ 
    if (channel.finishConnect()) 
    { 
    key.interestOps(0); // or SelectionKey.OP_READ or OP_WRITE, whatever is appropriate 
    } 
} 

Một vài ý kiến ​​không đầy đủ sau khi xem xét mã mở rộng của bạn:

  1. bế một kênh hủy chìa khóa. Bạn không cần phải làm cả hai.

  2. Phương thức removeInterest() không tĩnh được triển khai không chính xác.

  3. TYPE_DEREGISTER_OBJECT cũng đóng kênh. Không chắc chắn nếu đó là những gì bạn thực sự dự định. Tôi đã nghĩ rằng nó chỉ nên hủy bỏ chìa khóa, và cần có một hoạt động riêng biệt để đóng kênh.

  4. Bạn đã đi quá mức trên các phương thức nhỏ và xử lý ngoại lệ. addInterest() và removeInterest() là các ví dụ hay. Họ bắt ngoại lệ, đăng nhập chúng, sau đó tiến hành như thể ngoại lệ đã không xảy ra, khi tất cả họ thực sự làm là thiết lập hoặc xóa một chút: một dòng mã. Và trên hết, nhiều người trong số họ có cả phiên bản tĩnh và không tĩnh. Tương tự như vậy với tất cả các phương thức nhỏ gọi là key.cancel(), channel.close(), vv Không có điểm nào cho tất cả điều này, nó chỉ là tạo ra các dòng mã. Nó chỉ thêm tối nghĩa và làm cho mã của bạn khó hiểu hơn. Chỉ cần thực hiện thao tác bắt buộc nội dòng và có một trình thu duy nhất ở cuối vòng chọn.

  5. Nếu kết thúcConnect() trả về false thì đó không phải là lỗi kết nối, nó vẫn chưa hoàn thành. Nếu nó ném ra một ngoại lệ , là lỗi kết nối.

  6. Bạn đang đăng ký OP_CONNECT và OP_READ cùng một lúc. Điều này không có ý nghĩa và nó có thể gây ra vấn đề. Không có gì để đọc cho đến khi OP_CONNECT khởi chạy. Chỉ cần đăng ký OP_CONNECT lúc đầu.

  7. Bạn đang phân bổ ByteBuffer cho mỗi lần đọc. Điều này rất lãng phí. Sử dụng cùng một cho cuộc sống của kết nối.

  8. Bạn đang bỏ qua kết quả của tệp read(). Nó có thể bằng không. Có thể là -1, cho biết EOS, trên đó bạn phải đóng kênh. Bạn cũng giả sử bạn sẽ nhận được toàn bộ thông báo ứng dụng trong một lần đọc. Bạn không thể giả định điều đó. Đó là một lý do khác tại sao bạn nên sử dụng một ByteBuffer đơn cho tuổi thọ của kết nối.

  9. Bạn đang bỏ qua kết quả của write(). Nó có thể nhỏ hơn buffer.remaining() là khi bạn gọi nó. Nó có thể bằng không.

  10. Bạn có thể đơn giản hóa việc này rất nhiều bằng cách đặt NetSelectable thành tệp đính kèm khóa. Sau đó, bạn có thể loại bỏ một số thứ, bao gồm ví dụ bản đồ kênh và xác nhận, vì kênh của khóa phải luôn bằng kênh của phần đính kèm của khóa.

  11. Tôi cũng chắc chắn sẽ chuyển mã finishConnect() vào NetSelector và có connectEvent() chỉ là thông báo thành công/thất bại. Bạn không muốn lan truyền những thứ này xung quanh. Làm tương tự với readEvent(), tức là tự mình đọc trong NetSelector, với bộ đệm do NetSelectable cung cấp, và chỉ thông báo cho NetSelectable về kết quả đọc: count hoặc -1 hoặc ngoại lệ. Ditto on write: nếu kênh có thể ghi được, nhận một cái gì đó để viết từ NetSelectable, hãy viết nó trong NetSelector và thông báo kết quả.Bạn có thể có các cuộc gọi lại thông báo trả lại nội dung nào đó để cho biết phải làm gì tiếp theo, ví dụ: đóng kênh.

Nhưng điều này thực sự phức tạp gấp năm lần và thực tế là bạn có lỗi này chứng minh điều đó. Đơn giản hóa đầu của bạn.

+0

Có, kênh được đăng ký với 'Selector' ngay khi nó được mở và quan tâm đến' OP_CONNECT' được đặt sau khi lệnh gọi 'connect' (khác' ConnectionPendingException' được nâng lên). Chặn kết nối sẽ gây ra vấn đề cho tôi - các kết nối xảy ra vào thời điểm đặc biệt và nếu nó can thiệp vào I/O của các chủ đề khác thì điều đó thật tệ. – trojanfoe

+0

@trojanfoe Kênh cần được đăng ký và được đặt thành OP_CONNECT trước khi kết nối(), nếu không bạn có thể bỏ lỡ sự kiện. ConnectPendingException không thể được ném * trước * bạn đã gọi là connect() - Tôi không hiểu. Lý do cho ngoại lệ đó khá rõ ràng trong Javadoc và đây không phải là nó. – EJP

+0

Tôi đã thực hiện thay đổi đó và thử nghiệm nó dưới Linux (nó hoạt động) nhưng nó không khắc phục được vấn đề dưới Solaris. Tôi đã đề cập đến ngoại lệ sai - đó là NoConnectionPendingException nhưng điều đó không còn xảy ra vì vậy hãy bỏ qua điều đó. – trojanfoe