Professional Documents
Culture Documents
Báo cáo Lập trình mạng
Báo cáo Lập trình mạng
HÀ NỘI, 6/2021
Mục Lục
CHƯƠNG I: SOCKET FOR CLIENTS............................................4
1.1 Sử dụng socket............................................................................4
1.1.1 Điều tra giao thức với Telnet.............................................5
1.1.2 Đọc từ máy chủ có socket..................................................8
1.1.3 Viết vào máy chủ có socket..............................................18
1.2 Xây dựng và kết nối socket.......................................................26
1.2.1 Nhà xây dựng cơ bản.......................................................26
1.2.2 Xây dựng mà không cần kết nối.......................................29
1.2.3 Địa chỉ socket...................................................................31
1.2.4 Máy chủ Proxy.................................................................32
1.3 Nhận thông tin về socket...........................................................34
1.3.1 Đã đóng hoặc Đã kết nối?................................................36
1.4 Tùy chọn cài đặt socket.............................................................38
1.5 Ngoại lệ socket..........................................................................38
1.6 Socket trong ứng dụng GUI......................................................39
1.6.1 Whois...............................................................................40
1.6.2 A netwworl client library.................................................43
CHƯƠNG II: SOCKET FOR SERVERS.......................................51
2.1 Sử dụng ServerSockets..............................................................51
2.1.1 Phục vụ dữ liệu nhị phân..................................................57
2.1.2 Máy chủ đa luồng.............................................................58
2.1.3 Viết vào máy chủ có socket..............................................62
2.1.4Đóng socket Máy chủ.......................................................63
2.2 Đăng nhập.................................................................................66
2.2.1 what to log.......................................................................66
2.2.2 Cách đăng nhập................................................................67
2.3 Xây dựng server socket.............................................................70
2.3.1 Xây dựng mà không có ràng buộc....................................72
2.4 Lấy thông tin về server socket...................................................74
2.5 Tùy chọn socket........................................................................75
2.5.1 SO_TIMEOUT................................................................75
2.5.2 SO_REUSEADDR...........................................................76
2.5.3 SO_RCVBUF..................................................................76
2.5.4 Lớp dịch vụ......................................................................78
2.6 Máy chủ HTTP..........................................................................79
2.6.1 Máy chủ Một Tệp.............................................................80
2.6.2 Bộ chuyển hướng.............................................................84
2.6.3 Máy chủ HTTP chính thức...............................................86
CHƯƠNG I: SOCKET FOR CLIENTS
Dữ liệu được truyền qua Internet trong các gói có kích thước
hữu hạn được gọi là datagrams. Mỗi datagram chứa một tiêu đề và
một tải trọng. Tiêu đề chứa địa chỉ và cổng mà gói tin đang đi, địa
chỉ và cổng mà gói tin đến, séc để phát hiện tham nhũng dữ liệu và
nhiều thông tin vệ sinh khác được sử dụng để đảm bảo truyền tải
đáng tin cậy. Tải trọng chứa chính dữ liệu. Tuy nhiên, vì datagram
có độ dài hữu hạn, nên thường cần phải chia dữ liệu trên nhiều gói
và lắp ráp lại tại điểm đến. Cũng có thể một hoặc nhiều gói có thể bị
mất hoặc bị hỏng trong quá trình vận chuyển và cần phải được
truyền lại hoặc các gói đến ngoài trật tự và cần phải được sắp xếp
lại. Theo dõi điều này - chia dữ liệu thành các gói, tạo tiêu đề, phân
tích tiêu đề của các gói đến, theo dõi những gói nào đã và chưa được
nhận, v.v. - là rất nhiều công việc và đòi hỏi rất nhiều mã phức tạp.
May mắn thay, bạn không cần phải tự mình thực hiện công
việc. Socket cho phép lập trình viên xử lý kết nối mạng chỉ là một
luồng khác mà byte có thể được viết và từ đó có thể đọc byte. Socket
bảo vệ lập trình viên khỏi các chi tiết cấp thấp của mạng, chẳng hạn
như phát hiện lỗi, kích thước gói, tách gói, truyền lại gói, địa chỉ
mạng và hơn thế nữa.
Lớp Socket của Java , được sử dụng bởi cả khách hàng và máy chủ,
có các phương pháp tương ứng với bốn hoạt động đầu tiên trong số
này. Ba thao tác cuối cùng chỉ cần thiết bởi các máy chủ, chờ khách
hàng kết nối với họ. Chúng được thực hiện bởi lớp ServerSocket,
được thảo luận trong chương tiếp theo. Các chương trình Java
thường sử dụng socket máy khách theo cách sau:
• Chương trình tạo ra một socket mới với một constructor.
• Socket cố gắng kết nối với máy chủ từ xa.
Khi kết nối được thiết lập, các máy chủ cục bộ và từ xa sẽ nhận
được các luồng đầu vào và đầu ra từ socket và sử dụng các luồng đó
để gửi dữ liệu cho nhau. Kết nối này là full-duplex. Cả hai máy chủ
có thể gửi và nhận dữ liệu cùng một lúc. Dữ liệu có nghĩa là gì phụ
thuộc vào giao thức; các lệnh khác nhau được gửi đến máy chủ FTP
so với máy chủ HTTP. Thông thường sẽ có một số cái bắt tay được
thỏa thuận, tiếp theo là việc truyền dữ liệu từ cái này sang cái khác.
Khi việc truyền dữ liệu hoàn tất, một hoặc cả hai bên đóng kết nối.
Một số giao thức, chẳng hạn như HTTP 1.0, yêu cầu kết nối phải
được đóng sau mỗi yêu cầu được phục vụ. Những người khác, chẳng
hạn như FTP và HTTP 1.1, cho phép nhiều yêu cầu được xử lý trong
một kết nối duy nhất.
Bây giờ chúng ta hãy xem làm thế nào để truy xuất dữ liệu tương tự
này theo chương trình bằng cách sử dụng socket. Đầu tiên, mở
socket để time.nist.gov trên cổng 13:
Điều này không chỉ tạo ra đối tượng. Nó thực sự làm cho kết nối
trên mạng. Nếu kết nối hết thời gian hoặc thất bại vì máy chủ không
nghe trên cổng 13, thì người xây dựng sẽ ném IOException, vì vậy
bạn thường sẽ bọc điều này trong một khối thử. Trong Java 7,
Socket triển khai Autocloseable để bạn có thể sử dụng try-with-
resources:
Trong Java 6 trở về trước, bạn sẽ muốn đóng rõ ràng socket trong
một khối cuối cùng để giải phóng tài nguyên mà socket nắm giữ:
//ignore
}
}
}
Bước tiếp theo là tùy chọn nhưng rất được khuyến khích. Đặt thời
gian chờ trên kết nối bằng phương pháp setSoTimeout(). Thời gian
chờ được đo bằng mili giây, vì vậy câu lệnh này đặt socket hết thời
gian sau 15 giây không phản hồi:
socket. setSoTimeout(15000);
Mặc dù một socket sẽ ném ConnectException khá nhanh nếu máy
chủ từ chối kết nối hoặc NoRouteToHostException nếu các bộ định
tuyến không thể tìm ra cách gửi gói tin của bạn đến máy chủ, cả hai
đều không giúp bạn trong trường hợp máy chủ hoạt động sai chấp
nhận kết nối và sau đó ngừng nói chuyện với bạn mà không chủ
động đóng kết nối. Đặt thời gian chờ trên socket có nghĩa là mỗi lần
đọc hoặc ghi vào socket sẽ mất nhiều nhất một số mili giây nhất
định. Nếu một máy chủ bị treo trong khi bạn đang kết nối với nó,
bạn sẽ được thông báo bằng SocketTimeoutException. Chính xác
thời gian chờ bao lâu để thiết lập phụ thuộc vào nhu cầu của ứng
dụng của bạn và mức độ đáp ứng mà bạn mong đợi máy chủ. Mười
lăm giây là một thời gian dài để một máy chủ intranet cục bộ phản
hồi, nhưng nó khá ngắn đối với một máy chủ công cộng quá tải như
time.nist.gov.
Khi bạn đã mở socket và đặt thời gian chờ, hãy gọi getInputStream()
để trả về InputStream bạn có thể sử dụng để đọc byte từ socket. Nói
chung, một máy chủ có thể gửi bất kỳ byte nào cả; nhưng trong
trường hợp cụ thể này, giao thức quy định rằng các byte đó phải là
ASCII:
Ở đây tôi đã lưu trữ các byte trong stringbuilder. Tất nhiên, bạn có
thể sử dụng bất kỳ cấu trúc dữ liệu nào phù hợp với vấn đề của bạn
để giữ dữ liệu ra khỏi mạng.
Ví dụ 8-1 đặt tất cả điều này lại với nhau trong một chương trình
cũng cho phép bạn chọn một máy chủ ban ngày khác nhau.
Đầu ra điển hình giống như nếu bạn kết nối với Telnet:
$ java DaytimeClient
56375 13-03-24 15:05:42 50 0 843.6 UTC(NIST) *
Theo như mã mạng cụ thể, đó là khá nhiều nó. Trong hầu hết các
chương trình mạng như thế này, nỗ lực thực sự là nói giao thức và
hiểu các định dạng dữ liệu. Ví dụ: thay vì chỉ đơn giản là in ra văn
bản mà máy chủ gửi cho bạn, bạn có thể muốn phân tích nó thành
một đối tượng java.util.Date thay thế. Ví dụ 8-2 cho bạn thấy làm
thế nào để làm điều này. Đối với sự đa dạng, tôi cũng đã viết ví dụ
này tận dụng lợi thế của AutoCloseable của Java 7 và thử với các tài
nguyên.
Tuy nhiên, lưu ý rằng lớp này không thực sự làm bất cứ điều gì với
mạng mà Ví dụ 8-1 không làm. Nó chỉ cần thêm một loạt các mã để
biến chuỗi thành ngày. Khi đọc dữ liệu từ mạng, điều quan trọng cần
lưu ý là không phải tất cả các giao thức đều sử dụng ASCII hoặc
thậm chí là văn bản. Ví dụ, giao thức thời gian được chỉ định trong
RFC 868 quy định rằng thời gian được gửi là số giây kể từ nửa đêm,
ngày 1 tháng 1 năm 1900, Giờ trung bình Greenwich. Tuy nhiên,
điều này không được gửi dưới dạng chuỗi ASCII như 2.524.521.600
hoặc -1297728000. Thay vào đó, nó được gửi dưới dạng một số nhị
phân 32 bit, không có chữ ký, lớn.
Bởi vì giao thức thời gian không gửi lại văn bản, bạn không thể dễ
dàng sử dụng Telnet để kiểm tra một dịch vụ như vậy và chương
trình của bạn không thể đọc phản hồi máy chủ bằng Reader hoặc bất
kỳ loại phương pháp readLine() nào . Một chương trình Java kết
nối với các máy chủ thời gian phải đọc các byte thô và giải thích
chúng một cách thích hợp. Trong ví dụ này, công việc đó rất phức
tạp do Java thiếu loại số nguyên 32 bit không có chữ ký. Do đó, bạn
phải đọc byte một tại một thời điểm và chuyển đổi thủ công chúng
thành một thời gian dài bằng cách sử dụng các toán tử bitwise << và
|.
Ví dụ 8-3 cho thấy. Khi nói các giao thức khác, bạn có thể gặp phải
các định dạng dữ liệu thậm chí còn xa lạ hơn với Java. Ví dụ, một
vài giao thức mạng sử dụng số điểm cố định 64 bit. Không có phím
tắt để xử lý tất cả các trường hợp có thể xảy ra. Bạn chỉ cần nghiến
răng và mã hóa toán học bạn cần để xử lý dữ liệu ở bất kỳ định dạng
nào mà máy chủ gửi.
Dưới đây là kết quả của chương trình này từ một bản chạy mẫu:
$ java time
It í sun Mar 24 12:22:17 EDT 2013
Giao thức thời gian thực sự chỉ định Greenwich Mean Time, nhưng
phương pháp toString() trong lớp Date của Java, được
system.out.println ()ngầm gọi, chuyển đổi điều này thành múi giờ
của máy chủ địa phương, Eastern Daylight Time trong trường hợp
này.
1.1.3 Viết vào máy chủ có socket
Viết cho một máy chủ không khó hơn đáng kể so với đọc từ
một. Bạn chỉ cần yêu cầu socket cho một luồng đầu ra cũng như một
luồng đầu vào. Mặc dù có thể gửi dữ liệu qua socket bằng cách sử
dụng luồng đầu ra cùng một lúc bạn đang đọc dữ liệu qua luồng đầu
vào, hầu hết các giao thức được thiết kế để khách hàng đang đọc
hoặc viết qua socket, không phải cả hai cùng một lúc. Trong mô
hình phổ biến nhất, khách hàng gửi yêu cầu. Sau đó, server trả lời.
Khách hàng có thể gửi yêu cầu khác và máy chủ trả lời lại. Điều này
tiếp tục cho đến khi một bên hoặc bên kia được thực hiện và đóng
kết nối.
Một giao thức TCP hai chiều đơn giản là dict, được định nghĩa trong
RFC 2229. Trong giao thức này, máy khách mở một socket đến
cổng 2628 trên máy chủ dict và gửi các lệnh như "DEFINE eng-lat
gold". Điều này yêu cầu máy chủ gửi một định nghĩa về từ vàng
bằng cách sử dụng từ điển tiếng Anh sang tiếng Latinh của nó. (Các
máy chủ khác nhau có các từ điển khác nhau được cài đặt.) Sau khi
nhận được định nghĩa đầu tiên, khách hàng có thể yêu cầu một định
nghĩa khác. Khi nó được thực hiện, nó sẽ gửi lệnh "bỏ cuộc". Bạn có
thể khám phá dict với Telnet như thế này:
$ telnet dict.org 2628
Trying
216.18.20.172...
Connected to dict.org.
Bạn có thể thấy rằng các dòng phản hồi điều khiển bắt đầu bằng một
mã có ba chữ số. Định nghĩa thực tế là văn bản thuần túy, chấm dứt
với một khoảng thời gian trên một dòng của chính nó. Nếu từ điển
không chứa từ bạn yêu cầu, nó trả về 552 không khớp. Tất nhiên,
bạn cũng có thể tìm ra điều này, và nhiều hơn nữa, bằng cách đọc
RFC.
Không khó để thực hiện giao thức này trong Java. Đầu tiên, mở một
socket vào một máy chủ dict— _dict.org__ là một socket tốt—trên
cổng 2628:
Socket socket = new socket("dict.org", 2628);
Một lần nữa, bạn sẽ muốn đặt thời gian chờ trong trường hợp máy
chủ bị treo trong khi bạn đang kết nối với nó:
socket. setSoTimeout(15000);
Trong giao thức dict, khách hàng nói trước, vì vậy hãy yêu cầu
luồng đầu ra bằng cách sử dụng getOut putStream():
OutputStream out = socket. getOutputStream();
Phương pháp getOutputStream() trả về một OutputStream thô để
ghi dữ liệu từ ứng dụng của bạn sang đầu kia của socket. Bạn thường
xích luồng này sang một lớp thuận tiện hơn như DataOutputStream
hoặc OutputStreamWriter trước khi sử dụng nó. Vì lý do hiệu suất,
đó là một ý tưởng tốt để đệm nó là tốt. Bởi vì giao thức dict dựa trên
văn bản, cụ thể hơn là dựa trên UTF-8, thật thuận tiện để bọc điều
này trong Một Nhà văn:
Writer writer = New
OutputStreamWriter(out,"UTF-8");
Bây giờ viết lệnh trên socket:
write. write("DEFINE eng-lat gold\r\n");
Cuối cùng, xả đầu ra để bạn chắc chắn lệnh được gửi qua mạng:
writer.flush ();
Máy chủ bây giờ sẽ trả lời với một định nghĩa. Bạn có thể đọc rằng
bằng cách sử dụng luồng nhập của socket:
InputStream in = socket.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(in, "UTF-8"));
for (String line = reader.readLine();
!line.equals(".");
line = reader.readLine()) {
System.out.println(line);
}
Khi bạn nhìn thấy một khoảng thời gian trên một dòng của chính nó,
bạn biết định nghĩa là hoàn chỉnh. Sau đó, bạn có thể gửi bản thoát
qua luồng đầu ra:
writer.write ("quit\r\n");
writer.flush ();
Ví dụ 8-4 cho thấy một khách hàng hoàn toàn dict. Nó kết nối với
dict.org và dịch bất kỳ từ nào người dùng nhập vào dòng lệnh sang
tiếng Latinh. Nó lọc ra tất cả các dòng siêu dữ liệu bắt đầu bằng các
mã phản hồi như 150 hoặc 220. Tuy nhiên, nó đặc biệt kiểm tra một
dòng bắt đầu "552 không khớp" trong trường hợp máy chủ không
nhận ra từ đó.
Ví dụ 8-4. Một dịch giả tiếng Anh sang tiếng Latin dựa trên mạng
Đây là một mẫu chạy:
V
í dụ 8-4 là dòng định hướng. Nó đọc một dòng đầu vào từ bảng điều
khiển, gửi nó đến máy chủ và chờ đợi để đọc một dòng đầu ra mà nó
nhận được trở lại.
Socket nửa kín
Phương pháp đóng () tắt cả đầu vào và đầu ra từ socket. Đôi khi, bạn
có thể chỉ muốn tắt một nửa kết nối, đầu vào hoặc đầu ra. Các
phương pháp shutdownInput() và shutdownOutput() chỉ đóng một
nửa kết nối:
public void shutdownInput() throws IOException
public void shutdownOutput() throws IOException
Cả hai đều không thực sự đóng socket. Thay vào đó, họ điều chỉnh
luồng kết nối với socket để nó nghĩ rằng nó ở cuối luồng. Đọc thêm
từ luồng đầu vào sau khi tắt trả về đầu vào -1. Tiếp tục ghi vào
socket sau khi tắt đầu ra ném một IOException.
Nhiều giao thức, chẳng hạn như ngón tay, whois và HTTP, bắt đầu
với việc khách hàng gửi yêu cầu đến máy chủ, sau đó đọc phản hồi.
Có thể tắt đầu ra sau khi khách hàng đã gửi yêu cầu. Ví dụ: đoạn mã
này gửi yêu cầu đến máy chủ HTTP và sau đó tắt đầu ra, bởi vì nó sẽ
không cần phải viết bất cứ điều gì khác trên socket này:
Lưu ý rằng mặc dù bạn tắt một nửa hoặc thậm chí cả hai nửa của kết
nối, bạn vẫn cần phải đóng socket khi bạn đang thông qua với nó.
Các phương pháp tắt máy chỉ đơn giản là ảnh hưởng đến các luồng
của socket. Họ không giải phóng các tài nguyên liên quan đến
socket, chẳng hạn như cổng mà nó chiếm giữ.
Các phương pháp isInputShutdown() và isOutputShutdown() cho
bạn biết liệu các luồng đầu vào và đầu ra lần lượt được mở hay
đóng. Bạn có thể sử dụng chúng (thay vì được kết nối() và
isClosed()) để xác định cụ thể hơn xem bạn có thể đọc hoặc viết vào
socket:
public boolean isInputShutdown()
public boolean isOutputShutdown()
1.2 Xây dựng và kết nối socket
Lớp java.net.Socket là lớp cơ bản của Java để thực hiện các
hoạt động TCP phía máy khách. Các lớp hướng đến khách hàng khác
tạo ra các kết nối mạng TCP như URL, URLConnection, Applet và
JEditorPane cuối cùng đều kết thúc bằng cách gọi các phương pháp
của lớp này. Bản thân lớp này sử dụng mã gốc để giao tiếp với ngăn
xếp TCP cục bộ của hệ điều hành máy chủ.
Trong trình xây dựng này, đối số máy chủ chỉ là một tên máy chủ
được thể hiện dưới dạng Chuỗi. Nếu máy chủ tên miền không thể
giải quyết tên máy chủ hoặc không hoạt động, người xây dựng sẽ
ném UnknownHostException. Nếu socket không thể được mở vì một
số lý do khác, người xây dựng ném một IOException. Có nhiều lý do
khiến nỗ lực kết nối có thể thất bại: máy chủ mà bạn đang cố gắng
tiếp cận có thể không chấp nhận kết nối trên cổng đó, dịch vụ WiFi
của khách sạn có thể chặn bạn cho đến khi bạn đăng nhập vào trang
web của nó và trả 14,95 đô la hoặc các vấn đề định tuyến có thể ngăn
các gói của bạn đến đích.
Bởi vì trình xây dựng này không chỉ tạo ra một đối tượng
Socket mà còn cố gắng kết nối socket với máy chủ từ xa, bạn có thể
sử dụng đối tượng để xác định xem các kết nối đến một cổng cụ thể
có được phép hay không, như trong Ví dụ 8-5.
Ví dụ 8-5. Tìm hiểu cổng nào trong số 1024 cổng đầu tiên dường
như đang lưu trữ máy chủ TCP trên một máy chủ được chỉ định
Đây là đầu ra mà chương trình này tạo ra trên máy chủ địa phương
của tôi (kết quả của bạn sẽ khác nhau, tùy thuộc vào cổng nào bị
chiếm đóng):
Nếu bạn tò mò về những máy chủ nào đang chạy trên các cổng này,
hãy thử thử nghiệm với Telnet. Trên hệ thống Unix, bạn có thể tìm ra
dịch vụ nào nằm trên cổng nào bằng cách tìm kiếm trong tệp / vv /
dịch vụ. Nếu LowPortScanner tìm thấy bất kỳ cổng nào đang chạy
máy chủ nhưng không được liệt kê trong / etc / dịch vụ, thì điều đó
thật thú vị.
Mặc dù chương trình này trông đơn giản, nhưng nó không phải là
không có công dụng của nó. Bước đầu tiên để đảm bảo một hệ thống
là hiểu nó. Chương trình này giúp bạn hiểu những gì hệ thống của
bạn đang làm để bạn có thể tìm thấy (và đóng) các điểm vào có thể
cho kẻ tấn công. Bạn cũng có thể tìm thấy các máy chủ giả mạo: ví
dụ, LowPortScanner có thể cho bạn biết rằng có một máy chủ trên
cổng 800, trong đó, khi điều tra thêm, hóa ra là một máy chủ HTTP
mà ai đó đang chạy để phục vụ các tệp MP3 và đang bão hòa T1 của
bạn.
Ba nhà xây dựng tạo ra các socket không được kết nối. Chúng cung
cấp nhiều quyền kiểm soát hơn đối với chính xác cách socket cơ bản
hoạt động, ví dụ bằng cách chọn một máy chủ proxy khác hoặc sơ đồ
mã hóa:
1.2.2 Xây dựng mà không cần kết nối
Tất cả các nhà xây dựng mà chúng ta đã nói đến cho đến nay đều
tạo ra đối tượng socket và mở kết nối mạng với máy chủ từ xa. Đôi
khi bạn muốn chia nhỏ các hoạt động đó. Nếu bạn không đưa ra đối
số cho trình xây dựng Socket, nó không có nơi nào để kết nối với:
Public socket ()
Bạn có thể kết nối sau đó bằng cách chuyển SocketAddress đến một
trong các phương pháp kết nối (). Chẳng hạn:
Bạn có thể vượt qua một int là đối số thứ hai để chỉ định số mili giây
để chờ trước khi kết nối hết thời gian:
public void connect (SocketAddress endpoint, int timeout)
throws IOException
Mặc định, 0, có nghĩa là chờ đợi mãi mãi.
Raison d'être cho nhà xây dựng này là để cho phép các loại socket
khác nhau. Bạn cũng cần sử dụng nó để thiết lập một tùy chọn socket
chỉ có thể được thay đổi trước khi socket kết nối. Tôi sẽ thảo luận về
điều này trong "Thiết lập tùy chọn socket" trên trang 259. Tuy nhiên,
lợi ích chính mà tôi tìm thấy là nó cho phép tôi dọn dẹp mã trong các
khối cuối cùng, đặc biệt là trước Java 7. Các noargs constructor ném
không có ngoại lệ vì vậy nó cho phép bạn tránh kiểm tra null gây
phiền nhiễu khi đóng một socket trong một khối cuối cùng. Với
trình xây dựng ban đầu, hầu hết các mã trông như thế này:
Với nhà xây dựng mái vòm, nó trông như thế này:
Điều đó không hoàn toàn tốt đẹp như phiên bản autoclosing trong
Java 7, nhưng nó là một cải tiến.
1.2.3 Địa chỉ socket
Lớp SocketAddress đại diện cho một điểm cuối kết nối. Nó là
một lớp trừu tượng trống rỗng không có phương pháp nào ngoài một
trình xây dựng mặc định. Ít nhất về mặt lý thuyết, lớp
SocketAddress có thể được sử dụng cho cả socket TCP và không
TCP. Trong thực tế, chỉ có socket TCP / IP hiện đang được hỗ trợ và
các địa chỉ socket bạn thực sự sử dụng là tất cả các trường hợp của
InetSocketAddress.
Mục đích chính của lớp SocketAddress là cung cấp một cửa hàng
thuận tiện cho thông tin kết nối socket thoáng qua như địa chỉ IP và
cổng có thể được tái sử dụng để tạo socket mới, ngay cả sau khi
socket ban đầu bị ngắt kết nối và thu gom rác. Để đạt được điều này,
lớp Socket cung cấp hai phương pháp trả về các đối tượng
SocketAddress (getRemoteSocketAddress() trả về địa chỉ của hệ
thống được kết nối và getLocalSocketAddress() trả về địa chỉ mà từ
đó kết nối được thực hiện):
Public SocketAddress
getRemoteSocketAddress()
Public SocketAddress
getLocalSocketAddress()
Cả hai phương pháp này đều trả về null nếu socket chưa được kết
nối. Ví dụ: trước tiên bạn có thể kết nối với Yahoo! sau đó lưu trữ
địa chỉ của nó:
Socket socket = new Socket("www.yahoo.com", 80);
SocketAddress yahoo = socket.getRemoteSocketAddress();
socket.close();
Sau đó, bạn có thể kết nối lại với Yahoo! bằng địa chỉ này:
Socket socket2 = new Socket();
socket2.connect(yahoo);
Lớp InetSocketAddress (là lớp con duy nhất của SocketAddress trong
JDK và là lớp con duy nhất tôi từng gặp) thường được tạo bằng máy
chủ và cổng (dành cho khách hàng) hoặc chỉ là cổng (đối với máy chủ):
public InetSocketAddress(InetAddress address, int port)
public InetSocketAddress(String host, int port)
public InetSocketAddress(int port)
Bạn cũng có thể sử dụng phương pháp nhà máy tĩnh
InetSocketAddress.createUnresolved() để bỏ qua việc tra cứu máy
chủ trong DNS:
public static InetSocketAddress createUnresolved(String host,
int port)
InetSocketAddress có một vài phương pháp getter bạn có thể sử
dụng để kiểm tra đối tượng:
public final InetAddress getAddress()
public final int getPort()
public final String getHostName()
1.2.4 Máy chủ Proxy
Trình xây dựng cuối cùng tạo ra một socket không được kết nối kết
nối thông qua một máy chủ proxy được chỉ định:
Public socket (Proxy proxy)
Thông thường, máy chủ proxy mà socket sử dụng được điều khiển
bởi các thuộc tính hệ thống socksProxyHost và socksProxyPort , và
các thuộc tính này áp dụng cho tất cả các socket trong hệ thống. Tuy
nhiên, một socket được tạo bởi trình xây dựng này sẽ sử dụng máy
chủ proxy được chỉ định thay thế. Đáng chú ý nhất, bạn có thể vượt
qua Proxy.NO_PROXY cho đối số để bỏ qua tất cả các máy chủ
proxy hoàn toàn và kết nối trực tiếp với máy chủ từ xa. Tất nhiên,
nếu tường lửa ngăn chặn các kết nối trực tiếp, Java không thể làm gì
về nó; Và kết nối sẽ thất bại.
Để sử dụng một máy chủ proxy cụ thể, hãy chỉ định nó theo địa chỉ.
Ví dụ: đoạn mã này sử dụng máy chủ proxy SOCKS tại
myproxy.example.com để kết nối với máy chủ login.ibiblio.org:
SOCKS là loại proxy cấp thấp duy nhất mà Java hiểu được. Ngoài ra
còn có Proxy.Type.HTTP cấp cao hoạt động trong lớp ứng dụng
thay vì lớp vận chuyển và Proxy.Type.DIRECT đại diện cho các kết
nối không proxy.
1.3 Nhận thông tin về socket
Các đối tượng socket có một số thuộc tính có thể truy cập thông qua
các phương pháp getter:
• Địa chỉ từ xa
• Cổng từ xa
• Địa chỉ địa phương
• Cảng địa phương
Dưới đây là các phương pháp getter để truy cập các thuộc tính này:
Không có phương pháp setter. Các thuộc tính này được thiết lập
ngay khi socket kết nối và được cố định từ đó trở đi.
Các phương thức getInetAddress() và getPort() cho bạn biết máy
chủ từ xa và cổng socket được kết nối; hoặc, nếu kết nối hiện đã
đóng, máy chủ và cổng nào socket được kết nối khi nó được kết nối.
Các phương pháp getLocalAddress() và getLo calPort() cho bạn
biết giao diện mạng và cổng mà Socket được kết nối từ đó.
Không giống như cổng từ xa, (đối với socket khách hàng) thường là
một "cổng nổi tiếng" đã được ký trước bởi một ủy ban tiêu chuẩn,
cổng địa phương thường được hệ thống lựa chọn vào thời gian chạy
từ các cổng không sử dụng có sẵn. Bằng cách này, nhiều khách hàng
khác nhau trên một hệ thống có thể truy cập cùng một dịch vụ cùng
một lúc. Cổng cục bộ được nhúng trong các gói IP ra nước ngoài
cùng với địa chỉ IP của máy chủ cục bộ, vì vậy máy chủ có thể gửi
dữ liệu trở lại cổng bên phải trên máy khách.
Ví dụ 8-6 đọc một danh sách các tên máy chủ từ dòng lệnh, cố gắng
mở một socket cho mỗi cái, và sau đó sử dụng bốn phương pháp này
để in máy chủ từ xa, cổng từ xa, địa chỉ địa phương và cổng địa
phương.
Đây là kết quả của một cuộc chạy mẫu. Tôi đã bao gồm
www.oreilly.com trên đường dây chỉ huy hai lần để chứng minh rằng
mỗi kết nối được gán một cổng địa phương khác nhau, bất kể máy
chủ từ xa; cổng cục bộ được gán cho bất kỳ kết nối nào là không thể
đoán trước và phụ thuộc chủ yếu vào những cổng khác đang được sử
dụng. Kết nối với login.ibiblio.org thất bại vì máy đó không chạy bất
kỳ máy chủ nào trên cổng 80:
1.3.1 Đã đóng hoặc Đã kết nối?
Phương pháp isClosed() trả về đúng nếu socket được đóng, sai nếu
không. Nếu bạn không chắc chắn về trạng thái của socket, bạn có thể
kiểm tra nó bằng phương pháp này thay vì mạo hiểm ioexception.
Chẳng hạn:
if (socket. isClosed()) { // do something ...
} else { // do something else...
}
Tuy nhiên, đây không phải là một bài kiểm tra hoàn hảo. Nếu socket
chưa bao giờ được kết nối ngay từ đầu, isClosed () trả về sai, mặc dù
socket không chính xác mở.
Lớp Socket cũng có phương pháp isConnected(). Cái tên hơi gây
hiểu lầm. Nó không cho bạn biết nếu socket hiện đang được kết nối
với một máy chủ từ xa (như nếu nó là UNCLOSED). Thay vào đó,
nó cho bạn biết liệu socket đã từng được kết nối với máy chủ từ xa
hay chưa. Nếu socket có thể kết nối với máy chủ từ xa, phương pháp
này sẽ trở thành sự thật, ngay cả sau khi socket đó đã được đóng lại.
Để biết nếu một socket hiện đang mở, bạn cần kiểm tra xem
isConnected () trả về đúng và isClosed() trả về sai. Chẳng hạn:
boolean connected = socket.isConnected() && !
socket.isClosed();
Cuối cùng, phương pháp isBound () cho bạn biết liệu socket có bị
ràng buộc thành công với cổng đi trên hệ thống địa phương hay
không. Trong khi isConnected() đề cập đến đầu từ xa của socket,
isBound() đề cập đến kết thúc cục bộ. Điều này vẫn chưa quan
trọng lắm. Ràng buộc sẽ trở nên quan trọng hơn khi chúng ta thảo
luận về các socket máy chủ trong Chương 9.
1.4 Tùy chọn cài đặt socket
Tùy chọn socket chỉ định cách các socket gốc mà lớp Java Socket
dựa vào gửi và nhận dữ liệu. Java hỗ trợ chín tùy chọn cho socket
phía máy khách:
• TCP_NODELAY
• SO_BINDADDR
• SO_TIMEOUT
• SO_LINGER
• SO_SNDBUF
• SO_RCVBUF
• SO_KEEPALIVE
• OOBINLINE
• IP_TOS
Những cái tên trông buồn cười cho các tùy chọn này được lấy từ các
hằng số được đặt tên trong
Các tệp tiêu đề C được sử dụng ở Berkeley Unix, nơi các socket
được phát minh. Do đó, họ tuân theo các quy ước đặt tên Unix C cổ
điển thay vì các quy ước đặt tên Java dễ đọc hơn. Ví dụ,
SO_SNDBUF thực sự có nghĩa là "Tùy chọn socket gửi kích thước
bộ đệm."
Giao thức whois hỗ trợ một số cờ bạn có thể sử dụng để hạn chế
hoặc mở rộng tìm kiếm của mình. Ví dụ, nếu bạn biết bạn muốn tìm
kiếm một người tên là "Elliott" nhưng bạn không chắc chắn liệu anh
ta có đánh vần tên của mình là Elliot, Elliott, hoặc thậm chí có thể là
một cái gì đó không thể như Elliotte, bạn sẽ gõ:
1.6.2 A netwworl client library
Tốt nhất là nghĩ về các giao thức mạng như whois về các bit và byte
di chuyển trên mạng, cho dù là gói, dữ liệu hoặc luồng. Không có
giao thức mạng nào phù hợp với GUI (ngoại trừ Giao thức Khung từ
xa được sử dụng bởi VNC và X11). Nó thường là tốt nhất để gói gọn
mã mạng vào một thư viện riêng biệt mà mã GUI có thể gọi khi cần
thiết.
Ví dụ 8-7 là một lớp Whois có thể tái sử dụng . Hai trường xác định
trạng thái của mỗi đối tượng Whois: vật chủ, đối tượng InetAddress
và cổng, một int. Cùng nhau, chúng xác định máy chủ mà đối tượng
Whois cụ thể này kết nối với. Năm nhà xây dựng thiết lập các
trường này từ các kết hợp khác nhau của các đối số. Hơn nữa, máy
chủ có thể được thay đổi bằng cách sử dụng phương pháp se
tHost().
Chức năng chính của lớp nằm trong một phương pháp,
lookUpNames(). Phương pháp lookUp Names() trả về chuỗi chứa
đáp ứng whois cho một truy vấn đã cho. Các đối số chỉ định chuỗi
cần tìm kiếm, loại bản ghi nào cần tìm kiếm, cơ sở dữ liệu nào cần
tìm kiếm và liệu có cần khớp chính xác hay không. Tôi có thể đã sử
dụng chuỗi hoặc hằng số int để chỉ định loại bản ghi để tìm kiếm và
cơ sở dữ liệu để tìm kiếm, nhưng vì chỉ có một số lượng nhỏ các giá
trị hợp lệ, lookUpNames () định nghĩa enums với một số lượng
thành viên cố định thay thế. Giải pháp này cung cấp kiểm tra loại
thời gian biên dịch chặt chẽ hơn nhiều và đảm bảo lớp Whois sẽ
không phải xử lý một giá trị bất ngờ.
Ví dụ 8-7. Lớp Whois
Hình 8-1 cho thấy một giao diện có thể cho một máy khách whois
đồ họa phụ thuộc vào Ví dụ 8-7 cho các kết nối mạng thực tế. Giao
diện này có một trường văn bản để nhập tên cần tìm kiếm và hộp
kiểm để xác định xem trận đấu nên chính xác hay một phần. Một
nhóm các nút radio cho phép người dùng chỉ định nhóm bản ghi nào
họ muốn tìm kiếm. Một nhóm nút radio khác chọn các trường cần
tìm kiếm. Theo mặc định, máy khách này tìm kiếm tất cả các trường
của tất cả các bản ghi để khớp chính xác.
Khi người dùng nhập chuỗi trong Whois: hộp tìm kiếm và nhấn
Enter hoặc bấm nút Tìm, chương trình sẽ tạo kết nối với máy chủ
whois và truy xuất các bản ghi khớp với chuỗi đó. Chúng được đặt
trong khu vực văn bản ở dưới cùng của cửa sổ. Ban đầu, máy chủ
được đặt thành whois.internic.net, nhưng người dùng có thể tự do
thay đổi cài đặt này. Ví dụ 8-8 là chương trình sản xuất giao diện
này.
Phương pháp chính () là khối mã thông thường để khởi động một
ứng dụng độc lập. Nó xây dựng một đối tượng Whois và sau đó sử
dụng nó để xây dựng một đối tượng WhoisGUI. Sau đó, nhà xây
dựng WhoisGUI () thiết lập giao diện Swing. Có rất nhiều mã dự
phòng ở đây, vì vậy nó được chia thành các phương pháp riêng tư
initSearchFields(), initServer Choice(), makeSearchInRadioButton(),
và makeSearchForRadioButton(). Như thường lệ với các giao diện
dựa trên LayoutManager, việc thiết lập khá liên quan. Bởi vì bạn có
thể sẽ sử dụng một nhà thiết kế hình ảnh để xây dựng một ứng dụng
như vậy, tôi sẽ không mô tả nó chi tiết ở đây. Khi trình xây dựng trở
lại, phương pháp chính () gắn một lớp bên trong ẩn danh vào cửa sổ
sẽ đóng ứng dụng khi cửa sổ đóng. (Điều này không có trong trình
xây dựng vì các chương trình khác sử dụng lớp này có thể không
muốn thoát khỏi chương trình khi cửa sổ đóng.) chính () sau đó đóng
gói và hiển thị cửa sổ. Để tránh một điều kiện chủng tộc tối nghĩa có
thể dẫn đến bế tắc, điều này cần phải được thực hiện trong chủ đề
công văn sự kiện; do đó lớp bên trong FrameShower thực hiện
Runnable và cuộc gọi đến EventQueue.invokeLater(). Từ thời điểm
đó, tất cả các hoạt động diễn ra trong chủ đề công văn sự kiện.
Sự kiện đầu tiên mà chương trình này phải phản hồi là người dùng
gõ tên trong Whois: hộp tìm kiếm và nhấp vào nút Tìm hoặc nhấn
Enter. Trong trường hợp này, lớp bên trong Tra cứu Tên đặt văn bản
chính thành chuỗi trống và thực thi SwingWorker để thực hiện kết
nối mạng. SwingWorker (được giới thiệu trong Java 6) là một lớp
thực sự quan trọng để tìm hiểu xem bạn sẽ viết các ứng dụng GUI
truy cập mạng hoặc cho vấn đề đó thực hiện bất kỳ I / O nào cả.
Vấn đề swingworker giải quyết là điều này. Trong bất kỳ ứng dụng
Java GUI nào cũng có hai quy tắc bạn phải tuân theo để tránh bế tắc
và chậm chạp:
• Tất cả các bản cập nhật cho các thành phần Swing xảy ra trên
chủ đề công văn sự kiện.
• Không có hoạt động chặn chậm, đặc biệt là I / O, xảy ra trên chủ
đề công văn sự kiện. Nếu không, một máy chủ phản hồi chậm có
thể treo toàn bộ ứng dụng.
Hai quy tắc này mâu thuẫn với mã mạng và I / O nặng vì một phần
của mã thực hiện I / O không thể cập nhật GUI và ngược lại. Điều
này phải xảy ra trong hai chủ đề khác nhau.
Có một số cách để tránh nghịch lý này, nhưng trước Java 6, tất cả
chúng đều khá phức tạp. Tuy nhiên, trong Java 6 trở lên, giải pháp
rất dễ dàng. Xác định một lớp con của Swing Worker và ghi đè lên
hai phương pháp:
1. Một ServerSocket mới được tạo trên một cổng cụ thể bằng cách
sử dụng trình xây dựng ServerSocket().
2. ServerSocket lắng nghe các nỗ lực kết nối đến trên cổng đó bằng
cách sử dụng phương pháp chấp nhận () của nó. chấp nhận ()
chặn cho đến khi khách hàng cố gắng thực hiện kết nối, tại thời
điểm đó chấp nhận () trả về đối tượng Socket kết nối máy khách
và máy chủ.
3. Tùy thuộc vào loại máy chủ, phương pháp getInputStream () của
Socket, phương pháp getOutputStream() hoặc cả hai đều được
gọi để có được các luồng đầu vào và đầu ra giao tiếp với khách
hàng.
4. Máy chủ và máy khách tương tác theo một giao thức đã thỏa
thuận cho đến khi đến lúc đóng kết nối.
5. Máy chủ, máy khách hoặc cả hai đều đóng kết nối.
6. Máy chủ trở lại bước 2 và chờ kết nối tiếp theo.
Hãy chứng minh với một trong những giao thức đơn giản
hơn, ban ngày. Hãy nhớ lại từ Chương 8 rằng một máy chủ ban ngày
nghe trên cổng 13. Khi máy khách kết nối, máy chủ sẽ gửi thời gian
ở định dạng con người có thể đọc được và đóng kết nối. Ví dụ: đây
là kết nối với máy chủ ban ngày tại time-a.nist.gov:
$ telnet time-a.nist.gov 13
Trying 129.6.15.28...
Connected to time-a.nist.gov.
Escape character is '^]'.
53
www.it-ebooks.info
56375 13-03-24 13:37:50 50 0 0 888.8 UTC(NIST) *
Connection closed by foreign host.
Triển khai máy chủ ban ngày của riêng bạn rất dễ dàng. Đầu tiên,
tạo socket máy chủ nghe trên cổng 13:
ServerSocket server = New ServerSocket(13);
Tiếp theo, chấp nhận kết nối:
socket connection = server.accept ();
Các khối cuộc gọi chấp nhận (). Đó là, chương trình dừng lại ở đây
và chờ đợi, có thể trong nhiều giờ hoặc nhiều ngày, cho đến khi
khách hàng kết nối trên cổng 13. Khi máy khách thực hiện kết nối,
phương thức chấp nhận() trả về đối tượng Socket.
54
www.it-ebooks.info
Lưu ý rằng kết nối được trả về một đối tượng java.net.Socket , giống
như bạn đã sử dụng cho khách hàng trong chương trước. Giao thức
ban ngày yêu cầu máy chủ (và chỉ máy chủ) nói chuyện, vì vậy hãy
lấy OutputStream từ socket. Bởi vì giao thức ban ngày yêu cầu văn
bản, hãy chuỗi điều này với OutputStreamWriter:
OutputStream out = connection. getOutputStream();
Writer = New OutputStreamWriter(writer, "ASCII");
Bây giờ lấy thời gian hiện tại và viết nó lên luồng. Giao thức ban
ngày không yêu cầu bất kỳ định dạng cụ thể nào khác ngoài việc nó
có thể đọc được của con người, vì vậy hãy để Java chọn cho bạn:
Date now = new Date();
out.write(now.toString() +"\r\n");
Tuy nhiên, lưu ý việc sử dụng cặp return/linefeed vận chuyển để
chấm dứt đường dây. Đây hầu như luôn là những gì bạn muốn trong
một máy chủ mạng. Bạn nên chọn rõ ràng điều này thay vì sử dụng
bộ tách đường hệ thống, cho dù rõ ràng với System.getProper ty
("line.separator") hoặc ngầm thông qua một phương pháp như
println(). Cuối cùng, xả kết nối và đóng nó:
out.flush();
connection.close();
Bạn sẽ không phải lúc nào cũng phải đóng kết nối chỉ sau một lần
viết. Ví dụ, nhiều giao thức, dict và HTTP 1.1, cho phép khách hàng
gửi nhiều yêu cầu qua một socket duy nhất và mong đợi máy chủ gửi
nhiều phản hồi. Một số giao thức như FTP thậm chí có thể giữ một
socket mở vô thời hạn. Tuy nhiên, giao thức ban ngày chỉ cho phép
một phản hồi duy nhất.
Nếu máy khách đóng kết nối trong khi máy chủ vẫn đang hoạt động,
các luồng đầu vào và/hoặc đầu ra kết nối máy chủ với máy khách sẽ
ném một tion InterruptedIOExcep vào lần đọc hoặc ghi tiếp theo.
Trong cả hai trường hợp, máy chủ sau đó sẽ sẵn sàng để xử lý kết
nối đến tiếp theo.
Tất nhiên, bạn sẽ muốn làm tất cả điều này nhiều lần, vì vậy bạn sẽ
đặt tất cả điều này trong một vòng lặp. Mỗi lần đi qua vòng lặp gọi
phương pháp chấp nhận () một lần. Điều này trả về một đối tượng
Socket đại diện cho kết nối giữa máy khách từ xa và máy chủ cục
bộ. Tương tác với khách hàng diễn ra thông qua đối tượng Socket
này. Chẳng hạn:
Điều này được gọi là một máy chủ lặp đi lặp lại. Có một vòng lặp
lớn, và trong mỗi lần đi qua vòng lặp, một kết nối duy nhất được xử
lý hoàn toàn. Điều này hoạt động tốt cho một giao thức rất đơn giản
với các yêu cầu và phản hồi rất nhỏ như ban ngày, mặc dù ngay cả
với giao thức đơn giản này, một khách hàng chậm có thể trì hoãn các
khách hàng nhanh hơn khác. Các ví dụ sắp tới sẽ giải quyết vấn đề
này với nhiều chủ đề hoặc I /O không đồng bộ.
Khi xử lý ngoại lệ được thêm vào, mã trở nên phức tạp hơn một
chút. Điều quan trọng là phải phân biệt giữa các trường hợp ngoại lệ
có thể nên tắt máy chủ và ghi lại thông báo lỗi và các trường hợp
ngoại lệ chỉ cần đóng kết nối hoạt động đó. Các trường hợp ngoại lệ
trong phạm vi của một kết nối cụ thể nên đóng kết nối đó, nhưng
không ảnh hưởng đến các kết nối khác hoặc tắt máy chủ. Các trường
hợp ngoại lệ nằm ngoài phạm vi của một yêu cầu riêng lẻ có lẽ nên
tắt máy chủ. Để tổ chức điều này, lồng các khối thử :
Luôn luôn đóng một socket khi bạn đã hoàn thành với nó. Trong
Chương 8, tôi đã nói rằng một khách hàng không nên dựa vào phía
bên kia của kết nối để đóng socket; điều đó tăng gấp ba lần cho các
máy chủ. Khách hàng hết giờ hoặc gặp sự cố; người dùng hủy giao
dịch; mạng lưới đi xuống trong thời kỳ hightraffic; tin tặc khởi động
các cuộc tấn công từ chối dịch vụ. Vì bất kỳ lý do nào trong số này
hoặc một trăm lý do nữa, bạn không thể dựa vào khách hàng để đóng
socket, ngay cả khi giao thức yêu cầu họ, điều này không.
2.1.1 Phục vụ dữ liệu nhị phân
Gửi dữ liệu nhị phân, phi văn bản không khó hơn đáng kể.
Bạn chỉ cần sử dụng đầu ra
Luồng mà viết một mảng byte chứ không phải là một Nhà
văn viết một Chuỗi. Ví dụ 9-2 thể hiện với một máy chủ thời gian
lặp đi lặp lại theo giao thức thời gian được nêu trong RFC 868. Khi
một máy khách kết nối, máy chủ sẽ gửi một số nguyên 4 byte, big-
endian, không có chữ ký chỉ định số giây đã trôi qua kể từ 12:00
A.M., ngày 1 tháng 1 năm 1900, GMT (kỷ nguyên). Một lần nữa,
thời gian hiện tại được tìm thấy bằng cách tạo một đối tượng Ngày
mới. Tuy nhiên, vì lớp Date của Java có số mili giây kể từ 12:00
A.M., ngày 1 tháng 1 năm 1970, GMT thay vì vài giây kể từ 12:00
A.M., ngày 1 tháng 1 năm 1900, GMT, một số chuyển đổi là cần
thiết.
Như với TimeClient của chương trước, hầu hết các nỗ lực ở đây đi
vào hoạt động với một định dạng dữ liệu (số nguyên 32 bit không
được ký) mà Java không hỗ trợ bản địa.
2.1.2 Máy chủ đa luồng
Ban ngày và thời gian đều là những giao thức rất nhanh. Máy
chủ gửi tối đa vài chục byte và sau đó đóng kết nối. Nó hợp lý ở đây
để xử lý từng kết nối đầy đủ trước khi chuyển sang kết nối tiếp theo.
Tuy nhiên, ngay cả trong trường hợp đó, có thể một máy khách
chậm hoặc bị lỗi có thể treo máy chủ trong vài giây cho đến khi
nhận thấy socket bị hỏng. Nếu việc gửi dữ liệu có thể mất một
khoảng thời gian đáng kể ngay cả khi khách hàng và máy chủ đang
hoạt động, bạn thực sự không muốn mỗi kết nối phải chờ đợi kết nối
tiếp theo.
Các máy chủ Unix kiểu cũ như wu-ftpd tạo ra một quy trình mới để
xử lý từng kết nối để nhiều khách hàng có thể được phục vụ cùng
một lúc. Các chương trình Java nên sinh ra một luồng để tương tác
với máy khách để máy chủ có thể sẵn sàng xử lý kết nối tiếp theo
sớm hơn. Một luồng đặt một tải nhỏ hơn nhiều trên máy chủ so với
một quá trình con hoàn chỉnh. Trên thực tế, chi phí của việc bỏ quá
nhiều quy trình là lý do tại sao máy chủ Unix FTP điển hình không
thể xử lý hơn khoảng 400 kết nối mà không làm chậm thu thập dữ
liệu. Mặt khác, nếu giao thức đơn giản và nhanh chóng và cho phép
máy chủ đóng kết nối khi nó đi qua, nó sẽ hiệu quả hơn cho máy chủ
để xử lý yêu cầu của khách hàng ngay lập tức mà không cần sinh ra
một luồng.
Hệ điều hành lưu trữ các yêu cầu kết nối đến được gửi đến
một cổng cụ thể trong hàng đợi đầu tiên, đầu tiên. Theo mặc định,
Java đặt độ dài của hàng đợi này thành 50, mặc dù nó có thể thay đổi
từ hệ điều hành đến hệ điều hành. Một số hệ điều hành (không phải
Solaris) có chiều dài hàng đợi tối đa. Ví dụ, trên FreeBSD, chiều dài
hàng đợi tối đa mặc định là 128. Trên các hệ thống này, độ dài hàng
đợi cho socket máy chủ Java sẽ là hệ điều hành lớn nhất cho phép có
giá trị nhỏ hơn hoặc bằng 50. Sau khi hàng đợi lấp đầy dung lượng
với các kết nối chưa được xử lý, máy chủ từ chối các kết nối bổ sung
trên cổng đó cho đến khi các khe trong hàng đợi mở ra. Nhiều (mặc
dù không phải tất cả) khách hàng sẽ cố gắng thực hiện kết nối nhiều
lần nếu nỗ lực ban đầu của họ bị từ chối. Một số trình xây dựng
ServerSocket cho phép bạn thay đổi độ dài của hàng đợi nếu chiều
dài mặc định của nó không đủ lớn. Tuy nhiên, bạn sẽ không thể tăng
hàng đợi vượt quá kích thước tối đa mà hệ điều hành hỗ trợ. Tuy
nhiên, bất kể kích thước hàng đợi là gì, bạn muốn có thể làm trống
nó nhanh hơn các kết nối mới đang đến, ngay cả khi phải mất một
thời gian để xử lý từng kết nối.
Giải pháp ở đây là cung cấp cho mỗi kết nối chủ đề riêng của
nó, tách biệt với chủ đề chấp nhận các kết nối đến vào hàng đợi. Ví
dụ: Ví dụ 9-3 là một máy chủ ban ngày sinh ra một luồng mới để xử
lý từng kết nối đến. Điều này ngăn cản một khách hàng chậm chặn
tất cả các khách hàng khác. Đây là một chủ đề cho mỗi thiết kế kết
nối.
Ví dụ 9-3 sử dụng try-with-resources để tự động khóa socket máy
chủ. Tuy nhiên, nó cố tình không sử dụng tài nguyên thử với tài
nguyên cho các socket máy khách được chấp nhận bởi socket máy
chủ. Điều này là do socket khách hàng thoát khỏi khối thử thành
một chủ đề riêng biệt. Nếu bạn sử dụng thử với tài nguyên, chủ đề
chính sẽ đóng socket ngay khi nó kết thúc vòng lặp trong khi, có
khả năng trước khi chủ đề sinh ra đã hoàn thành việc sử dụng nó.
Tuy nhiên, thực sự có một cuộc tấn công từ chối dịch vụ vào máy
chủ này. Bởi vì Ví dụ 9-3 sinh ra một luồng mới cho mỗi kết nối,
nhiều kết nối đến gần như đồng thời có thể khiến nó sinh ra một số
lượng không xác định các luồng. Cuối cùng, máy ảo Java sẽ hết bộ
nhớ và gặp sự cố. Một cách tiếp cận tốt hơn là sử dụng một nhóm
luồng cố định như được mô tả trong Chương 3 để hạn chế việc sử
dụng tài nguyên tiềm năng. Năm mươi chủ đề nên là rất nhiều.
2.1.3 Viết vào máy chủ có socket
Trong các ví dụ cho đến nay, máy chủ chỉ viết cho socket của
khách hàng. Nó đã không đọc từ họ. Tuy nhiên, hầu hết các giao
thức yêu cầu máy chủ phải làm cả hai. Điều này không khó. Bạn sẽ
chấp nhận kết nối như trước đây, nhưng lần này yêu cầu cả
InputStream và Out putStream. Đọc từ khách hàng bằng Cách sử
dụng InputStream và viết vào nó bằng cách sử dụng Out putStream.
Bí quyết chính là hiểu giao thức: khi nào nên viết và khi nào nên
đọc.
Giao thức echo, được định nghĩa trong RFC 862, là một trong
những dịch vụ TCP tương tác đơn giản nhất.
Máy khách mở socket đến cổng 7 trên máy chủ echo và gửi dữ liệu.
Máy chủ gửi dữ liệu trở lại. Điều này tiếp tục cho đến khi khách
hàng đóng kết nối. Giao thức echo rất hữu ích để kiểm tra mạng để
đảm bảo rằng dữ liệu không bị gián đoạn bởi bộ định tuyến hoặc
tường lửa hoạt động sai. Bạn có thể kiểm tra echo với Telnet như thế
này:
Mẫu này
được định hướng dòng bởi vì đó là cách Telnet hoạt động. Nó đọc
một dòng đầu vào từ bảng điều khiển, gửi nó đến máy chủ, sau đó
chờ đợi để đọc một dòng đầu ra mà nó nhận được trở lại. Tuy nhiên,
giao thức echo không yêu cầu điều này. Nó lặp lại mỗi byte khi nó
nhận được nó. Nó không thực sự quan tâm cho dù những byte đại
diện cho các nhân vật trong một số mã hóa hoặc được chia thành các
dòng. Không giống như nhiều giao thức, echo không chỉ định hành
vi lockstep nơi khách hàng gửi yêu cầu nhưng sau đó chờ phản hồi
đầy đủ của máy chủ trước khi gửi thêm bất kỳ dữ liệu nào.
Không giống như ban ngày và thời gian, trong giao thức
echo, khách hàng có trách nhiệm đóng kết nối. Điều này làm cho nó
thậm chí còn quan trọng hơn để hỗ trợ hoạt động không đồng bộ với
nhiều luồng vì một khách hàng duy nhất có thể vẫn được kết nối vô
thời hạn.
Bao gồm ngoại lệ thay vì chỉ là một tin nhắn là tùy chọn nhưng
thông thường khi đăng nhập từ một khối bắt .
Có bảy cấp độ được định nghĩa là hằng số được đặt tên trong
java.util.logging.Level theo thứ tự giảm dần của độ nghiêm trọng:
Tôi sử dụng thông tin cho nhật ký kiểm toán và cảnh báo
hoặc nghiêm trọng cho nhật ký lỗi. Mức thấp hơn chỉ dành cho gỡ
lỗi và không nên được sử dụng trong các hệ thống sản xuất. Thông
tin, nghiêm trọng và cảnh báo đều có các phương pháp trợ giúp
thuận tiện đăng nhập ở mức đó. Ví dụ: câu lệnh này ghi lại một lượt
truy cập bao gồm ngày và địa chỉ từ xa:
Bạn có thể sử dụng bất kỳ định dạng nào thuận tiện cho các bản ghi
nhật ký riêng lẻ. Nói chung, mỗi bản ghi phải chứa dấu thời gian, địa
chỉ khách hàng và bất kỳ thông tin cụ thể nào cho yêu cầu đang được
xử lý. Nếu thông báo nhật ký đại diện cho một lỗi, hãy bao gồm
ngoại lệ cụ thể đã được ném. Java điền vào vị trí trong mã nơi tin
nhắn được đăng nhập tự động, vì vậy bạn không cần phải lo lắng về
điều đó.
2.3 Xây dựng server socket
Các nhà xây dựng này chỉ định cổng, độ dài của hàng đợi được sử
dụng để giữ các yêu cầu kết nối đến và giao diện mạng cục bộ để
liên kết. Hầu như tất cả chúng đều làm điều tương tự, mặc dù một số
sử dụng các giá trị mặc định cho độ dài hàng đợi và địa chỉ để liên
kết.
Ví dụ: để tạo socket máy chủ sẽ được sử dụng bởi một máy chủ
HTTP trên cổng 80, bạn sẽ viết:
ServerSocket httpd =new ServerSocket(80);
Để tạo socket máy chủ sẽ được sử dụng bởi một máy chủ HTTP trên
cổng 80 và xếp hàng lên đến 50 kết nối không được chấp nhận cùng
một lúc:
ServerSocket httpd =new ServerSocket (80, 50);
Nếu bạn cố gắng mở rộng hàng đợi qua chiều dài hàng đợi tối
đa của hệ điều hành, chiều dài hàng đợi tối đa được sử dụng thay
thế.
Theo mặc định, nếu máy chủ có nhiều giao diện mạng hoặc địa chỉ
IP, socket máy chủ sẽ nghe trên cổng được chỉ định trên tất cả các
giao diện và địa chỉ IP. Tuy nhiên, bạn có thể thêm đối số thứ ba để
chỉ ràng buộc với một địa chỉ IP cục bộ cụ thể. Đó là, socket máy
chủ chỉ nghe các kết nối đến trên địa chỉ được chỉ định; nó sẽ không
lắng nghe các kết nối đi vào thông qua các địa chỉ khác của máy chủ.
Ví dụ, login.ibiblio.org là một hộp Linux cụ thể ở Bắc
Carolina. Nó được kết nối với Internet với địa chỉ IP 152.2.210.122.
Cùng một hộp có thẻ Ethernet thứ hai với địa chỉ IP cục bộ
192.168.210.122 không hiển thị từ Internet công cộng, chỉ từ mạng
cục bộ. Nếu, vì một lý do nào đó, bạn muốn chạy một máy chủ trên
máy chủ này chỉ phản hồi các kết nối cục bộ từ trong cùng một
mạng, bạn có thể tạo một socket máy chủ nghe trên cổng 5776 của
192.168.210.122 nhưng không phải trên cổng 5776 của
152.2.210.122, như vậy:
InetAddress local = InetAddress.
getByName("192.168.210.122");
ServerSocket httpd =new ServerSocket (5776, 10, local);
Trong cả ba nhà xây dựng, bạn có thể vượt qua 0 cho số cổng
để hệ thống sẽ chọn một cổng có sẵn cho bạn. Một cổng được hệ
thống chọn như thế này đôi khi được gọi là cổng ẩn danh vì bạn
không biết trước số của nó (mặc dù bạn có thể tìm hiểu sau khi cổng
đã được chọn). Điều này thường hữu ích trong các giao thức đa
mông như FTP. Trong FTP thụ động, máy khách đầu tiên kết nối với
một máy chủ trên cổng nổi tiếng 21, vì vậy máy chủ phải chỉ định
cổng đó. Tuy nhiên, khi một tệp cần được chuyển, máy chủ bắt đầu
nghe trên bất kỳ cổng có sẵn nào. Máy chủ sau đó cho khách hàng
biết cổng nào khác mà nó nên kết nối với dữ liệu bằng cách sử dụng
kết nối lệnh đã mở trên cổng 21. Do đó, cổng dữ liệu có thể thay đổi
từ phiên này sang phiên tiếp theo và không cần phải biết trước.
(Active FTP tương tự ngoại trừ khách hàng nghe trên một cổng phù
du để máy chủ kết nối với nó, thay vì ngược lại.)
Tất cả các nhà xây dựng này ném một IOException, cụ thể,
một BindException, nếu socket không thể được tạo ra và bị ràng
buộc với cổng được yêu cầu. IoException khi tạo ServerSocket hầu
như luôn có nghĩa là một trong hai điều. Hoặc là một socket máy chủ
khác, có thể từ một chương trình hoàn toàn khác, đã sử dụng cổng
được yêu cầu hoặc bạn đang cố gắng kết nối với cổng từ 1 đến 1023
trên Unix (bao gồm Linux và Mac OS X) mà không có đặc quyền
root (superuser).
Bạn có thể tận dụng điều này để viết một biến thể về chương trình
LowPortScanner của chương trước. Thay vì cố gắng kết nối với một
máy chủ chạy trên một cổng nhất định, thay vào đó bạn cố gắng mở
một máy chủ trên cổng đó. Nếu nó bị chiếm đóng, nỗ lực sẽ thất bại.
.
Việc sử dụng chính cho tính năng này là cho phép các chương trình
thiết lập các tùy chọn socket máy chủ trước khi ràng buộc vào cổng.
Một số tùy chọn được cố định sau khi socket máy chủ đã bị ràng
buộc. Mô hình chung trông như thế này:
Bạn cũng có thể vượt qua null cho SocketAddress để chọn một cổng
tùy ý. Điều này giống như vượt qua 0 cho số cổng trong các nhà xây
dựng khác.
2.4 Lấy thông tin về server socket
Lớp ServerSocket cung cấp hai phương pháp getter cho bạn
biết địa chỉ địa phương và cổng bị chiếm đóng bởi socket máy chủ.
Chúng rất hữu ích nếu bạn đã mở socket máy chủ trên cổng ẩn danh
và / hoặc giao diện mạng không xác định. Đây sẽ là trường hợp, ví
dụ, trong kết nối dữ liệu của phiên FTP:
public InetAddress getInetAddress()
Phương thức này trả về địa chỉ đang được sử dụng bởi máy
chủ (máy chủ cục bộ). Nếu máy chủ cục bộ có một địa chỉ IP duy
nhất (như hầu hết), đây là địa chỉ được trả về bởi InetAd
dress.getLocalHost(). Nếu máy chủ cục bộ có nhiều hơn một địa chỉ
IP, địa chỉ cụ thể được trả về là một trong những địa chỉ IP của máy
chủ. Bạn không thể dự đoán địa chỉ nào bạn sẽ nhận được. Chẳng
hạn:
ServerSocket httpd = new ServerSocket (80);
InetAddress ia = httpd. getInetAddress();
Nếu ServerSocket chưa bị ràng buộc với giao diện mạng,
phương thức này trả về null:
Public int getLocalPort()
Các công cụ xây dựng ServerSocket cho phép bạn nghe trên
một cổng không xác định bằng cách vượt qua 0 cho số cổng. Phương
pháp này cho phép bạn tìm hiểu cổng nào bạn đang nghe. Bạn có thể
sử dụng điều này trong một chương trình đa trang ngang hàng, nơi
bạn đã có một phương tiện để thông báo cho các đồng nghiệp khác
về vị trí của bạn. Hoặc một máy chủ có thể sinh ra một số máy chủ
nhỏ hơn để thực hiện các hoạt động cụ thể. Máy chủ nổi tiếng có thể
thông báo cho khách hàng về những cổng họ có thể tìm thấy các
máy chủ nhỏ hơn. Tất nhiên, bạn cũng có thể sử dụng getLocalPort()
để tìm một cổng nonanonymous, nhưng tại sao bạn cần phải?
2.5 Tùy chọn socket
Tùy chọn socket chỉ định cách các socket gốc mà lớp
ServerSocket dựa vào gửi và nhận dữ liệu. Đối với socket máy chủ,
Java hỗ trợ ba tùy chọn:
• SO_TIMEOUT
• SO_REUSEADDR
• SO_RCVBUF
Nó cũng cho phép bạn đặt tùy chọn hiệu suất cho các gói của socket.
2.5.1 SO_TIMEOUT
SO_TIMEOUT là lượng thời gian, tính bằng mili giây, chấp
nhận () chờ kết nối đến trước khi ném
java.io.InterruptedIOException. Nếu SO_TIMEOUT là 0, chấp nhận
() sẽ không bao giờ hết thời gian. Mặc định là không bao giờ hết thời
gian.
Thiết lập SO_TIMEOUT là không phổ biến. Bạn có thể cần
nó nếu bạn đang thực hiện một giao thức phức tạp và an toàn đòi hỏi
nhiều kết nối giữa máy khách và máy chủ, nơi các phản hồi cần thiết
để xảy ra trong một khoảng thời gian cố định. Tuy nhiên, hầu hết các
máy chủ được thiết kế để chạy trong khoảng thời gian không xác
định và do đó chỉ sử dụng giá trị thời gian chờ mặc định, 0 (không
bao giờ hết thời gian). Nếu bạn muốn thay đổi điều này, phương
pháp setSoTi meout() đặt trường SO_TIMEOUT cho đối tượng
socket máy chủ này:
Đếm ngược bắt đầu khi chấp nhận () được gọi. Khi hết thời gian
chờ, ac cept() ném SocketTimeoutException, một lớp con của
IOException. Bạn cần đặt tùy chọn này trước khi gọi chấp nhận();
bạn không thể thay đổi giá trị thời gian chờ trong khi ac cept() đang
chờ kết nối. Đối số thời gian chờ phải lớn hơn hoặc bằng 0; nếu
không, phương pháp này sẽ ném một IllegalArgumentException.
2.5.2 SO_REUSEADDR
Tùy chọn SO_REUSEADDR cho socket máy chủ rất giống
với tùy chọn tương tự cho socket máy khách, được thảo luận trong
chương trước. Nó xác định xem một socket mới sẽ được phép liên
kết với một cổng đã sử dụng trước đó trong khi vẫn có thể có dữ liệu
đi qua mạng được gửi đến socket cũ. Như bạn có thể mong đợi, có
hai phương pháp để có được và thiết lập tùy chọn này:
Giá trị mặc định phụ thuộc vào nền tảng. Đoạn mã này xác định giá
trị mặc định bằng cách tạo ServerSocket mới và sau đó gọi
getReuseAddress():
Trên các hộp Linux và Mac OS X nơi tôi đã thử nghiệm mã này, các
socket máy chủ có thể tái sử dụng theo mặc định.
2.5.3 SO_RCVBUF
Tùy chọn SO_RCVBUF đặt kích thước bộ đệm nhận mặc
định cho socket máy khách được socket máy chủ chấp nhận. Nó
được đọc và viết bằng hai phương pháp sau:
Cài đặt SO_RCVBUF trên socket máy chủ giống như gọi
setReceiveBufferSize() trên mỗi socket riêng lẻ được trả về bằng
chấp nhận () (ngoại trừ việc bạn không thể thay đổi kích thước bộ
đệm nhận được sau khi socket đã được chấp nhận). Nhớ lại từ
chương trước rằng tùy chọn này gợi ý một giá trị cho kích thước của
các gói IP riêng lẻ trong luồng. Các kết nối nhanh hơn sẽ muốn sử
dụng bộ đệm lớn hơn, mặc dù hầu hết thời gian giá trị mặc định là
tốt.
Bạn có thể đặt tùy chọn này trước hoặc sau khi socket máy chủ bị
ràng buộc, trừ khi bạn muốn đặt kích thước bộ đệm nhận lớn hơn
64K. Trong trường hợp đó, bạn phải đặt tùy chọn trên ServerSocket
không bị ràng buộc trước khi ràng buộc nó.
2.5.4 Lớp dịch vụ
Như bạn đã biết trong chương trước, các loại dịch vụ Internet khác nhau có nhu
cầu hiệu suất khác nhau. Ví dụ, phát trực tiếp video thể thao cần băng thông tương đối
cao. Mặt khác, một bộ phim vẫn có thể cần băng thông cao nhưng có thể chịu được độ trễ
và độ trễ cao hơn. Email có thể được truyền qua các kết nối băng thông thấp và thậm chí
được giữ trong vài giờ mà không gây hại lớn. Bốn lớp giao thông chung được xác định
cho TCP:
• Chi phí thấp
• Độ tin cậy cao
• Thông lượng tối đa
• Độ trễ tối thiểu
Các lớp lưu lượng truy cập này có thể được yêu cầu cho một Socket nhất định. Ví
dụ: bạn có thể yêu cầu độ trễ tối thiểu có sẵn với chi phí thấp. Các biện pháp này đều mờ
nhạt và tương đối, không đảm bảo dịch vụ. Không phải tất cả các bộ định tuyến và ngăn
xếp TCP gốc đều hỗ trợ các lớp này.
Phương pháp setPerformancePreferences() thể hiện các ưu tiên tương đối dành cho
thời gian kết nối, độ trễ và băng thông cho các socket được chấp nhận trên máy chủ này:
Ví dụ:
bằng cách đặt kết nối Thời gian đến 2, độ trễ đến 1 và băng thông thành 3, bạn chỉ ra rằng
băng thông tối đa là đặc điểm quan trọng nhất, độ trễ tối thiểu là ít quan trọng nhất và
thời gian kết nối ở giữa:
ss. setPerformancePreferences(2, 1, 3);
Chính xác làm thế nào bất kỳ VM nào thực hiện điều này là phụ thuộc vào việc
thực hiện. Việc thực hiện socket cơ bản không bắt buộc phải tôn trọng bất kỳ yêu cầu nào
trong số này. Họ chỉ cung cấp một gợi ý cho ngăn xếp TCP về chính sách mong muốn.
Nhiều triển khai bao gồm Android bỏ qua các giá trị này hoàn toàn.
2.6 Máy chủ HTTP
Phần này hiển thị một số máy chủ HTTP khác nhau mà bạn có thể xây dựng với
các socket máy chủ, mỗi máy có một mục đích đặc biệt khác nhau và mỗi máy chủ phức
tạp hơn một chút so với trước đó.
HTTP là một giao thức lớn. Như bạn đã thấy trong Chương 5, một máy chủ HTTP
đầy đủ tính năng phải trả lời các yêu cầu về tệp, chuyển đổi URL thành tên tệp trên hệ
thống cục bộ, trả lời các yêu cầu POST và GET, xử lý các yêu cầu đối với các tệp không
tồn tại, giải thích các loại MIME và nhiều, nhiều hơn nữa. Tuy nhiên, nhiều máy chủ
HTTP không cần tất cả các tính năng này.
Ví dụ: nhiều trang web chỉ đơn giản là hiển thị thông báo "đang được xây dựng".
Rõ ràng, Apache là quá mức cần thiết cho một trang web như thế này. Một trang web như
vậy là một ứng cử viên cho một máy chủ tùy chỉnh chỉ làm một điều. Thư viện lớp mạng
của Java làm cho việc viết các máy chủ đơn giản như thế này gần như tầm thường.
Máy chủ tùy chỉnh không chỉ hữu ích cho các trang web nhỏ. Các trang web có lưu lượng
truy cập cao như Yahoo! cũng là ứng cử viên cho các máy chủ tùy chỉnh vì một máy chủ
chỉ làm một việc thường có thể nhanh hơn nhiều so với một máy chủ đa năng như
Apache hoặc Microsoft IIS. Thật dễ dàng để tối ưu hóa một máy chủ mục đích đặc biệt
cho một nhiệm vụ cụ thể; kết quả thường hiệu quả hơn nhiều so với một máy chủ đa năng
cần đáp ứng nhiều loại yêu cầu khác nhau. Ví dụ: các biểu tượng và hình ảnh được sử
dụng nhiều lần trên nhiều trang hoặc trên các trang có lưu lượng truy cập cao có thể được
xử lý tốt hơn bởi một máy chủ đọc tất cả các tệp hình ảnh vào bộ nhớ khi khởi động và
sau đó phục vụ chúng trực tiếp ra khỏi RAM, thay vì phải đọc chúng ra khỏi đĩa cho mỗi
yêu cầu. Hơn nữa, máy chủ này có thể tránh lãng phí thời gian vào việc ghi nhật ký nếu
bạn không muốn theo dõi các yêu cầu hình ảnh riêng biệt với các yêu cầu cho các trang
mà chúng được bao gồm.
Cuối cùng, Java không phải là một ngôn ngữ tồi cho các máy chủ web đầy đủ tính
năng nhằm cạnh tranh với apache hoặc IIS. Ngay cả khi bạn tin rằng các chương trình
Java chuyên sâu về CPU chậm hơn các chương trình C và C ++ chuyên sâu về CPU (điều
mà tôi rất nghi ngờ là đúng trong các máy ảo hiện đại), hầu hết các máy chủ HTTP đều bị
giới hạn bởi băng thông mạng và độ trễ, không phải bởi tốc độ CPU. Do đó, những lợi
thế khác của Java, chẳng hạn như bản chất nửa biên dịch / nửa giải thích của nó, tải lớp
năng động, thu gom rác và bảo vệ bộ nhớ thực sự có cơ hội tỏa sáng. Đặc biệt, các trang
web sử dụng nhiều nội dung động thông qua servlets, trang PHP hoặc các cơ chế khác
thường có thể chạy nhanh hơn nhiều khi được cài đặt lại trên một máy chủ web Java
thuần túy hoặc chủ yếu là thuần túy. Thật vậy, có một số máy chủ web sản xuất được viết
bằng Java, chẳng hạn như Jetty của Eclipse Foundation. Nhiều máy chủ web khác được
viết bằng C hiện nay bao gồm các thành phần Java đáng kể để hỗ trợ API Java Servlet và
Java Server Pages. Chúng phần lớn thay thế các CGIs truyền thống, ASP và phía máy
chủ bao gồm, chủ yếu là do các tương đương Java nhanh hơn và ít tốn tài nguyên hơn.
Tôi sẽ không khám phá những công nghệ này ở đây bởi vì chúng dễ dàng xứng đáng có
một cuốn sách của riêng họ. Tôi giới thiệu độc giả quan tâm đến Chương trình Java
Servlet của Jason Hunter (O'Reilly). Tuy nhiên, điều quan trọng cần lưu ý là các máy
chủ nói chung và máy chủ web nói riêng là một lĩnh vực mà Java thực sự cạnh tranh với
C về hiệu suất trong thế giới thực.