You are on page 1of 90

TRƯỜNG ĐẠI HỌC KIẾN TRÚC HÀ NỘI

KHOA CÔNG NGHỆ THÔNG TIN

BÁO CÁO CHI TIẾT

LẬP TRÌNH MẠNG


Đề tài :

SOCKET FOR CLIENTS


SOCKET FOR SERVERS

Thành viên nhóm :

1. Nguyễn Thị Thùy Linh 1855010094


2. Hoàng Trung Công 1855010013
3. Nguyễn Văn An 1855010001
4. Nguyễn Mạnh Thắng 1855010172

Giảng viên hướng dẫn : ThS. Nguyễn Mạnh Hù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.

1.1 Sử dụng socket


Socket là một kết nối giữa hai máy chủ. Nó có thể thực hiện bảy
thao tác cơ bản:
• Kết nối với máy từ xa
• Gửi dữ liệu
• Nhận dữ liệu
• Đóng kết nối
• Liên kết với cổng
• Lắng nghe dữ liệu đến
• Chấp nhận kết nối từ các máy từ xa trên cổng ràng buộc

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.

1.1.1 Điều tra giao thức với Telnet


Trong chương này, bạn sẽ thấy khách hàng sử dụng socket để
giao tiếp với một số dịch vụ Internet được biết đến như thời gian, sắc
lệnh và hơn thế nữa. Bản thân các socket đủ đơn giản; tuy nhiên, các
giao thức để giao tiếp với các máy chủ khác nhau làm cho cuộc sống
trở nên phức tạp.
Để có được cảm giác về cách một giao thức hoạt động, bạn có thể sử
dụng Telnet để kết nối với máy chủ, nhập các lệnh khác nhau cho nó
và xem phản hồi của nó. Theo mặc định, Telnet cố gắng kết nối với
cổng 23. Để kết nối với máy chủ trên các cổng khác nhau, hãy chỉ
định cổng bạn muốn kết nối như sau:
$ telnet localhost 25
Điều này yêu cầu kết nối với cổng 25, cổng SMTP, trên máy cục
bộ; SMTP là giao thức được sử dụng để chuyển email giữa các máy
chủ hoặc giữa máy khách thư và máy chủ. Nếu bạn biết các lệnh để
tương tác với máy chủ SMTP, bạn có thể gửi email mà không cần
thông qua chương trình thư. Thủ thuật này có thể được sử dụng để
giả mạo email. Ví dụ, một vài năm trước, các sinh viên mùa hè tại
Đài quan sát năng lượng mặt trời quốc gia ở Sunspot, New Mexico,
đã làm cho nó xuất hiện rằng bữa tiệc mà một trong những nhà khoa
học đã ném sau trận đấu bóng chuyền hàng năm giữa các nhân viên
và các sinh viên trên thực tế là một bữa tiệc chiến thắng cho các sinh
viên. (Tất nhiên, tác giả của cuốn sách này hoàn toàn không liên
quan gì đến hành vi đê hèn như vậy.;-) ) Sự tương tác với máy chủ
SMTP đã diễn ra như thế này; nhập các loại người dùng được hiển
thị in đậm (tên đã được thay đổi để bảo vệ người cả tin):
Một số nhân viên đã hỏi Bart tại sao anh ta, một nhân viên, lại tổ
chức một bữa tiệc chiến thắng cho các sinh viên. Đạo đức của câu
chuyện này là bạn không bao giờ nên tin tưởng email, đặc biệt là
email vô lý như thế này, mà không có xác minh độc lập. Trong 20
năm kể từ khi điều này xảy ra, hầu hết các máy chủ SMTP đã thêm
một chút bảo mật hơn so với hiển thị ở đây. Họ có xu hướng yêu cầu
tên người dùng và mật khẩu, và chỉ chấp nhận kết nối từ khách hàng
trong các mạng cục bộ và các máy chủ thư đáng tin cậy khác. Tuy
nhiên, bạn vẫn có thể sử dụng Telnet để mô phỏng máy khách, xem
máy khách và máy chủ tương tác như thế nào và do đó tìm hiểu xem
chương trình Java của bạn cần làm gì. Mặc dù phiên này không
chứng minh tất cả các tính năng của giao thức SMTP, nhưng nó đủ
để cho phép bạn suy luận cách một ứng dụng email đơn giản nói
chuyện với máy chủ.
1.1.2 Đọc từ máy chủ có socket
Hãy bắt đầu với một ví dụ đơn giản. Bạn sẽ kết nối với máy
chủ ban ngày tại Viện Tiêu chuẩn và Công nghệ Quốc gia (NIST) và
yêu cầu nó cho thời điểm hiện tại. Giao thức này được định nghĩa
trong RFC 867. Đọc điều đó, bạn thấy rằng máy chủ ban ngày nghe
trên cổng 13 và máy chủ gửi thời gian ở định dạng có thể đọc được
của con người và đóng kết nối. Bạn có thể kiểm tra máy chủ ban
ngày với Telnet như thế này:

Dòng "56375 13-03-24 13:37:50 50 0 888.8 UTC (NIST)" được gửi


bởi máy chủ ban ngày. Khi bạn đọc InputStream của Socket, đây là
những gì bạn sẽ nhận được. Các dòng khác được sản xuất bởi vỏ
Unix hoặc bởi chương trình Telnet.
RFC 867 không chỉ định bất kỳ định dạng cụ thể nào cho đầu ra
ngoài việc nó có thể đọc được của con người. Trong trường hợp này,
bạn có thể thấy kết nối này được thực hiện vào ngày 24 tháng 3 năm
2013, lúc 1:37: 50 P.M., Greenwich Meantime. Cụ thể hơn, định
dạng được định nghĩa là JJJJJ
YY-MM-DD HH:MM:SS TT L H msADV UTC(NIST) OTM trong đó:
• JJJJJ là "Ngày Julian sửa đổi" (tức là, đó là số ngày kể từ nửa
đêm ngày 17 tháng 11 năm 1858).
• YY-MM-DD là hai chữ số cuối cùng của năm, tháng và ngày hiện
tại của tháng.
• HH: MM: SS là thời gian tính bằng giờ, phút và giây trong Giờ
phối hợp toàn cầu (UTC, về cơ bản là Giờ trung bình
Greenwich).
• TT cho biết liệu Hoa Kỳ hiện đang tuân thủ giờ chuẩn hay giờ
tiết kiệm ánh sáng ban ngày: 00 có nghĩa là thời gian tiêu chuẩn;
50 có nghĩa là thời gian tiết kiệm ánh sáng ban ngày. Các giá trị
khác đếm ngược số ngày cho đến khi chuyển đổi.
• L là một mã có một chữ số cho biết liệu một giây nhuận sẽ được
thêm hoặc trừ vào nửa đêm vào ngày cuối cùng của tháng hiện
tại: 0 cho không có giây nhuận, 1 để thêm giây nhuận và 2 để trừ
đi một giây nhuận.
• H đại diện cho sức khỏe của máy chủ: 0 có nghĩa là khỏe mạnh,
1 có nghĩa là lên đến 5 giây tắt, 2 có nghĩa là hơn 5 giây tắt, 3 có
nghĩa là một số lượng không chính xác không xác định, và 4 là
chế độ bảo trì.
• msADV là một số mili giây mà NIST thêm vào thời gian nó gửi
để bù đắp cho sự chậm trễ của mạng. Trong mã trước, bạn có thể
thấy rằng nó đã thêm 888,8 mili giây vào kết quả này, bởi vì đó
là thời gian nó ước tính sẽ mất để phản hồi trở lại.
• Chuỗi UTC (NIST) là một hằng số, và OTM gần như là một hằng
số (một dấu hoa thị trừ khi có điều gì đó thực sự kỳ lạ đã xảy ra).
Những chi tiết này đều là NIST cụ thể. Chúng không phải là một
phần của tiêu chuẩn ban ngày. Mặc dù chúng cung cấp rất nhiều dữ
liệu, nhưng nếu bạn có nhu cầu lập trình thực sự để đồng bộ hóa với
máy chủ thời gian mạng, bạn nên sử dụng giao thức NTP được xác
định trong RFC 5905 thay thế.

Tôi không chắc ví dụ này sẽ hoạt động trong


bao lâu như được hiển thị ở đây. Các máy chủ
này bị quá tải và tôi đã gặp sự cố liên tục khi
kết nối trong khi viết chương này. Vào đầu năm 2013,
NISTđã đưa ra một danh từ, "Người dùng giao thức NIST
DAYTIME trên cổng TCP 13 cũng được khuyến khích
mạnh mẽ để nâng cấp lên giao thức thời gian mạng, cung
cấp độ chính xác cao hơn và yêu cầu băng thông mạng ít
hơn. Máy khách thời gian NIST (nistime-32bit.exe) hỗ trợ
cả hai giao thức. Chúng tôi hy vọng sẽ thay thế phiên bản
tcp của giao thức này bằng một phiên bản udpbased gần
cuối năm 2013". Tôi sẽ chỉ cho bạn cách truy cập dịch vụ
này qua UDP trong Chương 11.

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.

RFC không bao giờ thực sự xuất hiện và nói


rằng đây là định dạng được sử dụng. Nó chỉ
định 32 bit và giả định rằng bạn biết rằng tất cả
các giao thức mạng đều sử dụng số lớn. Thực tế là số
không được ký chỉ có thể được xác định bằng cách tính
ngày bao quanh cho các số nguyên đã ký và không ký và
so sánh nó với ngày được đưa ra trong đặc điểm kỹ thuật
(2036). Để làm cho vấn đề tồi tệ hơn, đặc điểm kỹ thuật
đưa ra một ví dụ về thời gian tiêu cực mà không thể thực
sự được gửi bởi các máy chủ thời gian tuân theo giao thức.
Thời gian là một giao thức tương đối cũ, được tiêu chuẩn
hóa vào đầu những năm 1980 trước khi IETF cẩn thận về
các vấn đề như ngày nay. Tuy nhiên, nếu bạn thấy mình
thực hiện một giao thức không được chỉ định rõ ràng, bạn
có thể phải thực hiện một số lượng thử nghiệm đáng kể so
với các triển khai hiện có để tìm ra những gì bạn cần làm.
Trong trường hợp xấu nhất, các triển khai khác nhau có thể
hoạt động khác nhau.

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ủ.

1.2.1 Nhà xây dựng cơ bản


Mỗi trình xây dựng Socket chỉ định máy chủ và cổng để kết
nối. Máy chủ có thể được chỉ định là InetAddress hoặc String. Cổng
từ xa được chỉ định là giá trị int từ 1 đến 65535:
public Socket(String host, int port) throws
UnknownHostException, IOException public Socket(InetAddress
host, int port) throws IOException
Các nhà xây dựng này kết nối socket (tức là, trước khi trình
xây dựng trở lại, kết nối mạng đang hoạt động được thiết lập với máy
chủ từ xa). Nếu kết nối không thể được mở vì một lý do nào đó,
người xây dựng sẽ ném IOException hoặc tion UnknownHostExcep.
Chẳng hạn:

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.

Ví dụ 8-6. Lấy thông tin của socket

Đâ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."

1.5 Ngoại lệ socket


public class SocketException extends IOException
Tuy nhiên, biết rằng một vấn đề xảy ra thường không đủ để giải
quyết vấn đề. Máy chủ từ xa có từ chối kết nối vì nó bận không? Có
phải máy chủ từ xa từ chối kết nối vì không có dịch vụ nào nghe trên
cổng? Kết nối đã cố gắng hết thời gian chờ vì tắc nghẽn mạng hoặc
vì máy chủ đã ngừng hoạt động? Có một số phân lớp của
SocketException cung cấp thêm thông tin về những gì đã xảy ra và
tại sao:
BindException được ném nếu bạn cố gắng xây dựng một đối tượng
Socket hoặc ServerSocket trên một cổng cục bộ đang được sử dụng
hoặc bạn không có đủ đặc quyền để sử dụng. Một Connec
tException được ném khi một kết nối bị từ chối tại máy chủ từ xa,
điều này thường xảy ra vì máy chủ bận rộn hoặc không có quy trình
nào đang nghe trên cổng đó. Cuối cùng, một
NoRouteToHostException chỉ ra rằng kết nối đã hết thời gian.
Gói java.net cũng bao gồm ProtocolException, một lớp con trực tiếp
của IOException:
public class ProtocolException extends IOException
Điều này được ném khi dữ liệu được nhận từ mạng bằng cách nào
đó vi phạm đặc điểm kỹ thuật TCP / IP.
Không có lớp ngoại lệ nào trong số này có bất kỳ phương pháp đặc
biệt nào bạn sẽ không tìm thấy trong bất kỳ lớp ngoại lệ nào khác,
nhưng bạn có thể tận dụng các lớp con này để cung cấp thêm thông
báo lỗi thông tin hoặc để quyết định xem việc thử lại hoạt động vi
phạm có khả năng thành công hay không.

1.6 Socket trong ứng dụng GUI


Trình duyệt web HotJava là máy khách mạng Java GUI quy mô lớn
đầu tiên. HotJava đã ngừng hoạt động, nhưng vẫn còn nhiều ứng
dụng khách nhận thức được mạng được viết bằng Java, bao gồm
Eclipse IDE và máy khách Frostwire BitTorrent. Hoàn toàn có thể
viết các ứng dụng khách hàng chất lượng thương mại trong Java; và
đặc biệt có thể viết các ứng dụng nhận thức mạng, cả khách hàng và
máy chủ. Phần này thể hiện một khách hàng mạng, whois, để minh
họa cho điểm này; và thảo luận về những cân nhắc đặc biệt phát sinh
khi tích hợp mã mạng với các ứng dụng Swing. Ví dụ dừng lại ngắn
của những gì có thể được thực hiện, nhưng chỉ trong giao diện người
dùng. Tất cả các mã mạng cần thiết đều có mặt. Thật vậy, một lần
nữa bạn phát hiện ra rằng mã mạng rất dễ dàng; đó là giao diện
người dùng khó khăn.
1.6.1 Whois
Whois là một giao thức dịch vụ thư mục đơn giản được xác định
trong RFC 954; ban đầu nó được thiết kế để theo dõi các quản trị
viên chịu trách nhiệm về máy chủ và tên miền Internet. Một máy
khách whois kết nối với một trong một số máy chủ trung tâm và yêu
cầu thông tin thư mục cho một người hoặc người; nó thường có thể
cung cấp cho bạn một số điện thoại, một địa chỉ email, và một địa
chỉ thư ốc sên (không nhất thiết phải là những người hiện tại, mặc
dù). Với sự phát triển bùng nổ của Internet, các lỗ hổng đã trở nên rõ
ràng trong giao thức whois, đáng chú ý nhất là bản chất tập trung
của nó. Một sự thay thế phức tạp hơn được gọi là whois ++ được ghi
lại trong RFCs 1913 và 1914 nhưng chưa được triển khai rộng
rãi.giao thức dịch vụ thư mục whois)
Hãy bắt đầu với một máy khách đơn giản để kết nối với máy chủ
whois. Cấu trúc cơ bản của giao thức WHOIS là:
1. Máy khách mở socket TCP đến cổng 43 trên máy chủ.
2. Máy khách gửi chuỗi tìm kiếm bị chấm dứt bởi cặp
return/linefeed vận chuyển (\r\n). Chuỗi tìm kiếm có thể là tên,
danh sách tên hoặc lệnh đặc biệt, như đã thảo luận trong thời gian
ngắn. Bạn cũng có thể tìm kiếm tên miền, như www.oreilly.com
hoặc netscape.com, cung cấp cho bạn thông tin về mạng.
3. Máy chủ gửi một lượng thông tin không xác định có thể đọc
được của con người để đáp ứng với lệnh và đóng kết nối.
4. Khách hàng hiển thị thông tin này cho người dùng.
Chuỗi tìm kiếm mà khách hàng gửi có định dạng khá đơn giản. Ở
mức cơ bản nhất, nó chỉ là tên của người mà bạn đang tìm kiếm.
Dưới đây là một tìm kiếm đơn giản cho "Harold":
Mặc dù đầu vào trước đó có định dạng khá rõ ràng, định dạng đó
đáng tiếc là không đạt tiêu chuẩn. Các máy chủ whois khác nhau có
thể và làm gửi đầu ra khác nhau. Ví dụ: đây là một vài kết quả đầu
tiên từ cùng một tìm kiếm tại máy chủ whois chính của Pháp,
whois.nic.fr:
Ở đây mỗi bản ghi đầy đủ được trả lại thay vì chỉ là một danh sách
các trang web. Các máy chủ whois khác có thể sử dụng các định
dạng khác. Giao thức này hoàn toàn không được thiết kế để xử lý
máy. Bạn khá nhiều phải viết mã mới để xử lý đầu ra của mỗi máy
chủ whois khác nhau. Tuy nhiên, bất kể định dạng đầu ra, mỗi phản
hồi có thể chứa một tay cầm, trong đầu ra Internic là một tên miền
và trong nic.fr đầu ra nằm trong trường nic-hdl. Tay cầm được đảm
bảo là duy nhất và được sử dụng để có được thông tin cụ thể hơn về
một người hoặc một mạng. Nếu bạn tìm kiếm một tay cầm, bạn sẽ
nhận được nhiều nhất một trận đấu. Nếu tìm kiếm của bạn chỉ có
một trận đấu, vì bạn may mắn hoặc bạn đang tìm kiếm một tay cầm,
máy chủ sẽ trả về một bản ghi chi tiết hơn. Đây là tìm kiếm
oreilly.com. Bởi vì chỉ có một oreilly.com trong cơ sở dữ liệu, máy
chủ trả về tất cả thông tin mà nó có trên tên miền này:

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.

Hình 8-1. Một máy khách whois đồ họa

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. Phương pháp doInBackground() thực hiện hoạt động dài hạn, I /


O nặng. Nó không tương tác với GUI. Nó có thể trả lại bất kỳ
loại thuận tiện và ném bất kỳ ngoại lệ.
2. Phương pháp đã hoàn thành() được tự động gọi trên luồng điều
phối sự kiện sau khi phương thức doInBackground() trở về, vì
vậy nó có thể cập nhật GUI. Phương pháp này có thể gọi phương
pháp get() để truy xuất giá trị trả về được tính bởi
doInBackground().

Ví dụ 8-8 sử dụng một lớp bên trong có tên Lookup là SwingWorker


của nó. Phương pháp doInBack ground() nói chuyện với máy chủ
whois và trả về phản hồi của máy chủ dưới dạng Chuỗi. Phương
pháp đã hoàn thành() cập nhật vùng văn bản tên với phản hồi của
máy chủ.
Sự kiện thứ hai mà chương trình này phải trả lời là người dùng gõ
máy chủ mới trong trường văn bản máy chủ. Trong trường hợp này,
một lớp bên trong ẩn danh cố gắng xây dựng một đối tượng Whois
mới và lưu trữ nó trong trường máy chủ. Nếu nó thất bại (ví dụ: vì
người dùng gõ sai tên máy chủ), máy chủ cũ sẽ được khôi phục. Một
hộp cảnh báo thông báo cho người dùng về sự kiện này.
Đây không phải là một khách hàng hoàn hảo bằng bất kỳ phương
tiện nào. Thiếu sót rõ ràng nhất là nó không cung cấp một cách để
lưu dữ liệu và thoát khỏi chương trình. Tuy nhiên, nó chứng minh
làm thế nào để thực hiện kết nối mạng một cách an toàn từ một
chương trình GUI mà không chặn chủ đề công văn sự kiện.
CHƯƠNG II: SOCKET FOR SERVERS
Chương trước đã thảo luận về các socket từ quan điểm của khách
hàng: các chương trình mở socket cho máy chủ đang lắng nghe các
kết nối. Tuy nhiên, bản thân socket của khách hàng là không đủ;
khách hàng không được sử dụng nhiều trừ khi họ có thể nói chuyện
với máy chủ và lớp Socket được thảo luận trong chương trước
không đủ để viết máy chủ. Để tạo socket, bạn cần biết máy chủ
Internet mà bạn muốn kết nối. Khi bạn đang viết một máy chủ, bạn
không biết trước ai sẽ liên hệ với bạn; Và ngay cả khi bạn đã làm,
bạn sẽ không biết khi nào chủ nhà đó muốn liên lạc với bạn. Nói
cách khác, các máy chủ giống như nhân viên tiếp tân ngồi bên điện
thoại và chờ đợi các cuộc gọi đến. Họ không biết ai sẽ gọi hoặc khi
nào, chỉ biết rằng khi điện thoại đổ chuông, họ phải nhấc nó lên và
nói chuyện với bất cứ ai ở đó. Bạn không thể lập trình hành vi đó chỉ
với lớp Socket.
Đối với các máy chủ chấp nhận kết nối, Java cung cấp lớp
ServerSocket đại diện cho socket máy chủ. Về bản chất, công việc
của socket máy chủ là ngồi cạnh điện thoại và chờ đợi các cuộc gọi
đến. Về mặt kỹ thuật, một socket máy chủ chạy trên máy chủ và lắng
nghe các kết nối TCP đến. Mỗi socket máy chủ nghe trên một cổng
cụ thể trên máy chủ. Khi một khách hàng trên một máy chủ từ xa cố
gắng kết nối với cổng đó, máy chủ thức dậy, đàm phán kết nối giữa
máy khách và máy chủ và trả về một đối tượng Socket thông
thường đại diện cho socket giữa hai máy chủ. Nói cách khác, socket
máy chủ chờ kết nối trong khi socket máy khách bắt đầu kết nối. Khi
ServerSocket đã thiết lập kết nối, máy chủ sử dụng đối tượng
Socket thông thường để gửi dữ liệu đến máy khách. Dữ liệu luôn di
chuyển qua socket thông thường.

2.1 Sử dụng ServerSockets


Lớp ServerSocket chứa mọi thứ cần thiết để viết máy chủ
trong Java. Nó có các nhà xây dựng tạo ra các đối tượng
ServerSocket mới , các phương pháp nghe các kết nối trên một cổng
được chỉ định, các phương pháp cấu hình các tùy chọn socket máy
chủ khác nhau và các phương pháp linh tinh thông thường như
toString().
Trong Java, vòng đời cơ bản của một chương trình máy chủ là:

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.

2.1.4 Đóng socket Máy chủ


Nếu bạn đã hoàn thành với một socket máy chủ, bạn nên
đóng nó, đặc biệt là nếu chương trình sẽ tiếp tục chạy trong một thời
gian. Điều này giải phóng cổng cho các chương trình khác có thể
muốn sử dụng nó. Đóng ServerSocket không nên nhầm lẫn với việc
đóng socket. Đóng ServerSocket giải phóng một cổng trên máy chủ
cục bộ, cho phép một máy chủ khác liên kết với cổng; nó cũng phá
vỡ tất cả các socket hiện đang mở mà ServerSocket đã chấp nhận.
Socket máy chủ được đóng tự động khi một chương trình chết, vì
vậy không hoàn toàn cần thiết để đóng chúng trong các chương trình
chấm dứt ngay sau khi ServerSocket không còn cần thiết nữa. Tuy
nhiên, nó không làm tổn thương. Các lập trình viên thường tuân theo
cùng một mô hình closeif-not-null trong một khối thử cuối cùng mà
bạn đã quen thuộc từ các luồng và socket phía máy khách:
}
}
Bạn có thể cải thiện điều này một chút bằng cách sử dụng trình xây
dựng vòm vĩnh ServerSocket(), không ném bất kỳ ngoại lệ nào và
không liên kết với cổng. Thay vào đó, bạn gọi phương pháp bind()
để liên kết với một địa chỉ socket sau khi đối tượng ServerSocket()
đã được xây dựng:

Trong Java 7, ServerSocket triển khai AutoCloseable để bạn có thể


tận dụng tài nguyên trywith thay thế:
try (ServerSocket server = new ServerSocket(port)) {
// ... làm việc với socket máy chủ
}
Sau khi một socket máy chủ đã được đóng, nó không thể
được kết nối lại, ngay cả với cùng một cổng.
Phương pháp isClosed() trả về đúng nếu ServerSocket đã bị
đóng, sai nếu nó không:
public boolean isClosed()
Các đối tượng ServerSocket được tạo ra với trình xây dựng noargs
ServerSocket() và chưa bị ràng buộc với một cổng không được coi là
đóng cửa. Invoking isClosed() trên các đối tượng này trả về sai.
Phương pháp isBound() cho bạn biết liệu ServerSock et có bị ràng
buộc với một cổng hay không:
public boolean isBound()
Như với phương pháp isBound () của lớp Socket được thảo luận
trong Chương 8, tên này hơi gây hiểu lầm. isBound() trả về đúng
nếu ServerSocket đã từng bị ràng buộc với một cổng, ngay cả khi nó
hiện đang đóng. Nếu bạn cần kiểm tra xem ServerSocket có mở hay
không, bạn phải kiểm tra cả hai trả về isBound() trả về đúng và trả
về isClosed() sai. Chẳng hạn:
public static boolean isOpen(ServerSocket ss) {
return ss. isBound() & ! ss. bị tịch thu();
}
2.2 Đăng nhập
Máy chủ chạy không giám sát trong thời gian dài. Nó thường quan
trọng để gỡ lỗi những gì đã xảy ra khi trong một máy chủ rất lâu sau
khi thực tế. Vì lý do này, nên lưu trữ nhật ký máy chủ trong ít nhất
một khoảng thời gian.

2.2.1 What to log


Có hai điều chính bạn muốn lưu trữ trong nhật ký của mình:
• Yêu cầu
• Lỗi hệ phục vụ
Thật vậy, các máy chủ thường giữ hai logfiles khác nhau cho
hai mục khác nhau này. Nhật ký kiểm toán thường chứa một mục
cho mỗi kết nối được thực hiện với máy chủ. Các máy chủ thực hiện
nhiều hoạt động trên mỗi kết nối có thể có một mục nhập cho mỗi
hoạt động thay thế. Ví dụ, một máy chủdict có thể ghi lại một mục
nhập cho mỗi từ mà khách hàng tìm kiếm. Nhật ký lỗi chứa hầu hết
các trường hợp ngoại lệ bất ngờ xảy ra trong khi máy chủ đang chạy.
Ví dụ: bất kỳ NullPointerException nào xảy ra nên được đăng nhập
ở đây vì nó chỉ ra một lỗi trong máy chủ bạn sẽ cần phải sửa chữa.
Nhật ký lỗi không chứa lỗi của khách hàng, chẳng hạn như máy
khách bất ngờ ngắt kết nối hoặc gửi yêu cầu bị dị dạng. Chúng đi
vào nhật ký yêu cầu. Nhật ký lỗi chỉ dành riêng cho các trường hợp
ngoại lệ bất ngờ.
Nguyên tắc chung của ngón tay cái cho nhật ký lỗi là mọi
dòng trong nhật ký lỗi nên được xem xét và giải quyết. Số lượng
mục nhập lý tưởng trong nhật ký lỗi là bằng không. Mỗi mục nhập
trong nhật ký này đại diện cho một lỗi cần được điều tra và giải
quyết. Nếu việc điều tra mục nhật ký lỗi kết thúc với quyết định rằng
ngoại lệ đó không thực sự là vấn đề và mã đang hoạt động như dự
định, hãy loại bỏ tuyên bố nhật ký. Nhật ký lỗi lấp đầy quá nhiều
báo động sai nhanh chóng trở nên bị bỏ qua và vô dụng.
Vì lý do tương tự, không giữ nhật ký gỡ lỗi trong sản xuất. Không
đăng nhập mỗi khi bạn nhập một phương pháp, mỗi khi một điều
kiện được đáp ứng, v.v. Không ai từng nhìn vào những khúc gỗ này.
Họ chỉ lãng phí không gian và che giấu những vấn đề thực sự. Nếu
bạn cần ghi nhật ký cấp phương pháp để gỡ lỗi, hãy đặt nó vào một
tệp riêng biệt và tắt nó trong tệp thuộc tính toàn cầu khi chạy trong
sản xuất.
Các hệ thống ghi nhật ký nâng cao hơn cung cấp các công cụ
phân tích nhật ký cho phép bạn thực hiện những việc như chỉ hiển
thị tin nhắn có THÔNG TIN ƯU TIÊN trở lên hoặc chỉ hiển thị các
tin nhắn có nguồn gốc từ một phần nhất định của mã. Những công
cụ này làm cho nó khả thi hơn để giữ một logfile hoặc cơ sở dữ liệu
duy nhất, thậm chí có thể chia sẻ một bản ghi giữa nhiều nhị phân
hoặc chương trình khác nhau. Tuy nhiên, nguyên tắc vẫn áp dụng
rằng một bản ghi nhật ký mà không ai sẽ nhìn vào là vô giá trị tốt
nhất và thường xuyên hơn là không gây mất tập trung hoặc gây
nhầm lẫn.
Đừng làm theo antipattern thông thường của việc ghi lại tất cả
mọi thứ bạn có thể nghĩ đến chỉ trong trường hợp ai đó có thể cần nó
một ngày nào đó. Trong thực tế, các lập trình viên rất tệ trong việc
đoán trước những thông điệp nhật ký mà họ có thể cần để gỡ lỗi các
vấn đề sản xuất. Một khi một vấn đề xảy ra, đôi khi rõ ràng những
thông điệp bạn cần; Nhưng rất hiếm khi có thể dự đoán trước điều
này. Thêm tin nhắn "chỉ trong trường hợp" vào logfiles thường có
nghĩa là khi một vấn đề xảy ra, bạn đang điên cuồng săn lùng các tin
nhắn có liên quan trong một biển dữ liệu thậm chí còn lớn hơn.

2.2.2 Cách đăng nhập


Nhiều chương trình kế thừa có niên đại từ Java 1.3 trở về
trước vẫn sử dụng các thư viện ghi nhật ký của bên thứ ba như log4j
hoặc Apache Commons Logging, nhưng gói java.util.logging có sẵn
kể từ Java 1.4 đủ cho hầu hết các nhu cầu. Chọn nó tránh được rất
nhiều sự phụ thuộc phức tạp của bên thứ ba.
Mặc dù bạn có thể tải một logger theo yêu cầu, nó thường dễ
dàng nhất để chỉ tạo ra một cho mỗi lớp như vậy:
private final static Logger auditLogger =
Logger.getLogger("requests");
Loggers là chủ đề an toàn, vì vậy không có vấn đề lưu trữ chúng trong
một trường tĩnh chia sẻ. Thật vậy, chúng gần như phải là bởi vì ngay
cả khi đối tượng Logger không được chia sẻ giữa các chủ đề, logfile
hoặc cơ sở dữ liệu sẽ được. Điều này rất quan trọng trong các máy
chủ đa luồng cao.
Ví dụ này xuất ra một nhật ký có tên là "yêu cầu". Nhiều đối
tượng Logger có thể xuất ra cùng một khúc gỗ, nhưng mỗi logger
luôn ghi lại chính xác một bản ghi. Nhật ký là gì và ở đâu phụ thuộc
vào cấu hình bên ngoài. Thông thường nhất đó là một tệp, có thể
hoặc không thể được đặt tên là "yêu cầu"; nhưng nó có thể là một cơ
sở dữ liệu, một dịch vụ SOAP chạy trên một máy chủ khác, một
chương trình Java khác trên cùng một máy chủ, hoặc một cái gì đó
khác.
Một khi bạn có một logger, bạn có thể viết cho nó bằng cách sử
dụng bất kỳ của một số phương pháp. Cơ bản nhất là log(). Ví dụ:
khối bắt này ghi lại một ngoại lệ thời gian chạy bất ngờ ở mức cao
nhất:

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:

• Level.SEVERE (giá trị cao nhất)


• Level.WARNING
• Level.INFO
• Level.CONFIG
• Level.FINE
• Level.FINER
• Level.FINEST (giá trị thấp nhất)

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.
.

2.3.1 Xây dựng mà không có ràng buộc


Trình xây dựng mái vòm tạo ra một đối tượng ServerSocket nhưng
không thực sự liên kết nó với một cổng, vì vậy ban đầu nó không thể
chấp nhận bất kỳ kết nối nào. Nó có thể bị ràng buộc sau này bằng
cách sử dụng các phương pháp bind() :

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.

2.6.1 Máy chủ Một Tệp


Cuộc điều tra của về các máy chủ HTTP bắt đầu với một máy chủ luôn gửi cùng
một tệp, bất kể yêu cầu nào. Nó được gọi là SingleFileHTTPServer và được hiển thị
trong Ví dụ 9-10. Tên tệp, cổng cục bộ và mã hóa nội dung được đọc từ dòng lệnh. Nếu
cổng bị bỏ qua, cổng 80 được giả định. Nếu mã hóa bị bỏ qua, ASCII được giả định.
Ví dụ 9-10. Một máy chủ HTTP phục vụ một tệp duy nhất
Các nhà xây dựng thiết lập dữ liệu được gửi cùng với tiêu đề HTTP bao gồm
thông tin về độ dài nội dung và mã hóa nội dung. Tiêu đề và cơ thể của phản ứng được
lưu trữ trong các mảng byte trong mã hóa mong muốn để chúng có thể được thổi cho
khách hàng rất nhanh chóng.
Lớp SingleFileHTTPServer giữ nội dung cần gửi, tiêu đề cần gửi và cổng để ràng
buộc. Phương pháp start() tạo ra một ServerSocket trên cổng được chỉ định, sau đó đi
vào một vòng lặp vô hạn liên tục chấp nhận các kết nối và xử lý chúng.
Mỗi socket đến được xử lý bởi một đối tượng Handler có thể chạy được gửi đến một
nhóm chủ đề. Do đó, một khách hàng chậm chạp không thể bỏ đói các khách hàng khác.
Mỗi Handler nhận được một InputStream từ nó mà nó đọc yêu cầu của khách hàng. Nó
nhìn vào dòng đầu tiên để xem liệu nó có chứa chuỗi HTTP hay không. Nếu nhìn thấy
chuỗi này, máy chủ giả định rằng máy khách hiểu HTTP/1.0 trở lên và do đó gửi tiêu đề
MIME cho tệp; Sau đó nó gửi dữ liệu. Nếu yêu cầu của máy khách không chứa chuỗi
HTTP, máy chủ sẽ bỏ qua tiêu đề, tự gửi dữ liệu. Cuối cùng, người xử lý đóng kết nối.
Phương pháp chính () chỉ đọc các tham số từ dòng lệnh. Tên của tệp được phục vụ
được đọc từ đối số dòng lệnh đầu tiên. Nếu không có tệp nào được chỉ định hoặc tệp
không thể mở được, thông báo lỗi sẽ được in và chương trình thoát ra. Giả sử tệp có thể
được đọc, nội dung của nó được đọc vào dữ liệu mảng byte bằng cách sử dụng các lớp
Đường dẫn và Tệp được giới thiệu trong Java 7. Lớp URLConnection đưa ra một dự
đoán hợp lý về loại nội dung của tệp và phỏng đoán đó được lưu trữ trong biếnType nội
dung. Tiếp theo, số cổng được đọc từ đối số dòng lệnh thứ hai. Nếu không có cổng nào
được chỉ định hoặc nếu đối số thứ hai không phải là số nguyên từ 1 đến 65.535, cổng 80
được sử dụng. Mã hóa được đọc từ đối số dòng lệnh thứ ba, nếu có. Nếu không, UTF-8
được giả định. Sau đó, các giá trị này được sử dụng để xây dựng một đối tượng
SingleFileHTTPServer và bắt đầu nó.
Phương pháp chính () chỉ là một giao diện có thể. Bạn có thể dễ dàng sử dụng lớp
học này như một phần của một số chương trình khác. Nếu bạn thêm phương pháp setter
để thay đổi nội dung, bạn có thể dễ dàng sử dụng nó để cung cấp thông tin trạng thái đơn
giản về máy chủ hoặc hệ thống đang chạy. Tuy nhiên, điều đó sẽ đặt ra một số vấn đề bổ
sung về an toàn luồng mà Ví dụ 9-10 không phải giải quyết vì dữ liệu là bất biến.
Dưới đây là những gì bạn thấy khi bạn kết nối với máy chủ này thông qua Telnet (các chi
tiết cụ thể phụ thuộc vào máy chủ và tệp chính xác):

2.6.2 Bộ chuyển hướng


Redirection là một ứng dụng đơn giản nhưng hữu ích khác cho một máy chủ
HTTP có mục đích đặc biệt.
Trong phần này, bạn phát triển một máy chủ chuyển hướng người dùng từ trang
web này sang trang web khác - ví dụ: từ cnet.com đến www.cnet.com. Ví dụ 9-11 đọc
URL và số cổng từ dòng lệnh, mở socket máy chủ trên cổng và chuyển hướng tất cả các
yêu cầu mà nó nhận được đến trang web được chỉ định bởi URL mới bằng mã 302
FOUND. Trong ví dụ này, tôi đã chọn sử dụng một chủ đề mới thay vì một nhóm chủ đề
cho mỗi kết nối. Điều này có lẽ đơn giản hơn một chút để viết mã và hiểu nhưng có phần
kém hiệu quả hơn
Tuy nhiên, nếu bạn kết nối với trình duyệt web, bạn nên được gửi đến
http://www.cafeconleche.org/ chỉ với một sự chậm trễ nhỏ. Bạn không bao giờ nên thấy
HTML được thêm vào sau mã phản hồi; điều này chỉ được cung cấp để hỗ trợ các trình
duyệt rất cũ không tự động chuyển hướng, cũng như một vài hoang tưởng bảo mật đã cấu
hình trình duyệt của họ không tự động chuyển hướng.
Phương pháp chính () cung cấp một giao diện rất đơn giản đọc URL của trang web
mới để chuyển hướng kết nối đến và cổng cục bộ để nghe. Nó sử dụng thông tin này để
xây dựng một đối tượng Redirector. Sau đó, nó gọi bắt đầu(). Nếu cổng không được chỉ
định, Redirector sẽ nghe trên cổng 80. Nếu trang web bị bỏ qua, Redirector sẽ in thông
báo lỗi và thoát ra.
Phương pháp khởi động () của Redirector liên kết socket máy chủ với cổng, in một
thông báo trạng thái ngắn và sau đó đi vào một vòng lặp vô hạn trong đó nó lắng nghe
các kết nối. Mỗi khi một kết nối được chấp nhận, đối tượng Socket kết quả được sử
dụng để xây dựng một Redi rectThread. RedirectThread này sau đó được bắt đầu. Tất cả
các tương tác tiếp theo với khách hàng diễn ra trong chủ đề mới này. Phương pháp bắt
đầu () sau đó chỉ cần chờ kết nối đến tiếp theo.
Phương pháp chạy () của RedirectThread thực hiện hầu hết các công việc. Nó bắt
đầu bằng cách xích một Nhà văn vào luồng đầu ra của Socket và Đầu đọc vào luồng đầu
vào của Socket. Cả đầu vào và đầu ra đều được đệm. Sau đó, phương pháp chạy () đọc
dòng đầu tiên mà khách hàng gửi. Mặc dù khách hàng có thể sẽ gửi toàn bộ tiêu đề
MIME, bạn có thể bỏ qua điều đó. Dòng đầu tiên chứa tất cả thông tin bạn cần. Dòng
trông giống như thế này:
NHẬN /directory/filename.html HTTP/1.0
Có thể từ đầu tiên sẽ là POST hoặc PUT thay thế hoặc sẽ không có phiên bản
HTTP. "Từ" thứ hai là tệp mà khách hàng muốn truy xuất. Điều này phải bắt đầu bằng
một dấu gạch chéo (/). Trình duyệt chịu trách nhiệm chuyển đổi URL tương đối thành
URL tuyệt đối bắt đầu bằng dấu cắt; Server không làm điều này. Từ thứ ba là phiên bản
của giao thức HTTP mà trình duyệt hiểu. Các giá trị có thể không là gì cả (trình duyệt
trước HTTP/1.0), HTTP/1.0 hoặc HTTP/1.1.
Để xử lý một yêu cầu như thế này, Redirector bỏ qua từ đầu tiên. Từ thứ hai được
đính kèm với URL của máy chủ đích (được lưu trữ trong trường newSite) để cung cấp
URL chuyển hướng đầy đủ. Từ thứ ba được sử dụng để xác định xem có nên gửi tiêu đề
MIME hay không; Tiêu đề MIME không được sử dụng cho các trình duyệt cũ không
hiểu HTTP/1.0. Nếu có phiên bản, tiêu đề MIME sẽ được gửi; nếu không, nó sẽ bị bỏ
qua.
Gửi dữ liệu là gần như tầm thường. Tác giả được sử dụng. Bởi vì tất cả dữ liệu bạn
gửi là ASCII thuần túy, mã hóa chính xác không quá quan trọng. Mẹo duy nhất ở đây là
ký tự cuối dòng cho các yêu cầu HTTP là \r\n–a carriage return theo sau là linefeed.
Các dòng tiếp theo mỗi dòng gửi một dòng văn bản cho khách hàng. Dòng đầu tiên được
in là:
TÌM THẤY HTTP/1.0 302
Đây là một mã phản hồi HTTP / 1.0 cho khách hàng biết mong đợi được chuyển hướng.
Dòng thứ hai là Date: tiêu đề cung cấp thời gian hiện tại tại máy chủ. Dòng này là tùy
chọn. Dòng thứ ba là tên và phiên bản của máy chủ; dòng này cũng là tùy chọn nhưng
được sử dụng bởi những con nhện cố gắng giữ số liệu thống kê về các máy chủ web phổ
biến nhất. Dòng tiếp theo là Vị trí: tiêu đề, được yêu cầu cho loại phản hồi này. Nó cho
khách hàng biết nơi nó đang được chuyển hướng đến. Cuối cùng là loại Nội dung tiêu
chuẩn: tiêu đề. Bạn gửi văn bản/html kiểu nội dung để chỉ ra rằng khách hàng nên mong
đợi để xem HTML. Cuối cùng, một dòng trống được gửi để biểu thị sự kết thúc của dữ
liệu tiêu đề.
Tất cả mọi thứ sau này sẽ là HTML, được xử lý bởi trình duyệt và hiển thị cho người
dùng. Một vài dòng tiếp theo in một thông báo cho các trình duyệt không hỗ trợ chuyển
hướng, vì vậy những người dùng đó có thể tự nhảy đến trang web mới. Thông điệp đó
trông giống như:

2.6.3 Máy chủ HTTP chính thức


Đủ máy chủ HTTP chuyên dụng. Phần tiếp theo này phát triển một máy chủ
HTTP toàn diện, được gọi là JHTTP, có thể phục vụ toàn bộ cây tài liệu, bao gồm hình
ảnh, applet, tệp HTML, tệp văn bản và hơn thế nữa. Nó sẽ rất giống với
SingleFileHTTPServer, ngoại trừ việc nó chú ý đến các yêu cầu GET. Máy chủ này vẫn
còn khá nhẹ; sau khi xem mã, chúng tôi sẽ thảo luận về các tính năng khác mà bạn có thể
muốn thêm.
Bởi vì máy chủ này có thể phải đọc và phục vụ các tệp lớn từ hệ thống tệp qua các
kết nối mạng có khả năng làm chậm, bạn sẽ thay đổi cách tiếp cận của nó. Thay vì xử lý
từng yêu cầu khi nó đến trong chủ đề thực hiện chính, bạn sẽ đặt các kết nối đến trong
một nhóm. Các phiên bản riêng biệt của lớp Bộ xử lý Yêu cầu sẽ loại bỏ các kết nối khỏi
nhóm và xử lý chúng. Ví dụ 9-12 hiển thị lớp JHTTP chính . Như trong hai ví dụ trước,
phương pháp chính () của JHTTP xử lý khởi tạo, nhưng các chương trình khác có thể sử
dụng lớp này để chạy các máy chủ web cơ bản
Phương pháp chính() của lớp JHTTP đặt thư mục gốc tài liệu từ args[0]. Cổng
được đọc từ args [1] hoặc 80 được sử dụng cho mặc định. Sau đó, một đối tượng JHTTP
mới được xây dựng và bắt đầu. JHTTP tạo một nhóm luồng để xử lý các yêu cầu và liên
tục chấp nhận các kết nối đến. Bạn gửi một luồng Bộ xử lý Yêu cầu cho mỗi kết nối đến
vào nhóm.
Mỗi kết nối được xử lý bằng phương pháp chạy () của lớp RequestProcessor được
hiển thị trong Ví dụ 9-13. Nó nhận được các luồng đầu vào và đầu ra từ socket và chuỗi
chúng cho người đọc và nhà văn. Người đọc đọc dòng đầu tiên của yêu cầu khách hàng
để xác định phiên bản HTTP mà khách hàng hỗ trợ—bạn chỉ muốn gửi tiêu đề MIME
nếu đây là HTTP/1.0 trở lên— và tệp được yêu cầu. Giả sử phương pháp là GET, tệp
được yêu cầu được chuyển đổi thành tên tệp trên hệ thống tệp cục bộ. Nếu tệp được yêu
cầu là thư mục (tức là tên của nó kết thúc bằng dấu gạch chéo), bạn thêm tên của tệp chỉ
mục. Bạn sử dụng đường dẫn kinh điển để đảm bảo rằng tệp được yêu cầu không đến từ
bên ngoài thư mục gốc tài liệu. Nếu không, một khách hàng lén lút có thể đi bộ trên tất cả
các hệ thống tập tin cục bộ bằng cách bao gồm .. trong URL để đi lên hệ thống phân cấp
thư mục. Đây là tất cả những gì bạn cần từ khách hàng, mặc dù một máy chủ web tiên
tiến hơn, đặc biệt là một máy chủ đã đăng nhập, sẽ đọc phần còn lại của tiêu đề MIME
mà khách hàng gửi.
Tiếp theo, tệp được yêu cầu được mở và nội dung của nó được đọc thành một
mảng byte . Nếu phiên bản HTTP là 1.0 trở lên, bạn viết tiêu đề MIME thích hợp trên
luồng đầu ra.
Để tìm ra loại nội dung, bạn gọi urlConnection.getFileNameMap().get
ContentTypeFor(fileName) phương pháp để lập bản đồ các phần mở rộng tệp như .html
vào các loại MIME như văn bản /html. Mảng byte chứa nội dung của tệp được ghi vào
luồng đầu ra và kết nối được đóng. Nếu tệp không thể tìm thấy hoặc mở, bạn sẽ gửi cho
khách hàng phản hồi 404. Nếu khách hàng gửi một phương pháp mà bạn không hỗ trợ,
chẳng hạn như POST, bạn sẽ gửi lại lỗi 501. Nếu có ngoại lệ xảy ra, bạn đăng nhập, đóng
kết nối và tiếp tục.
Máy chủ này có chức năng nhưng vẫn khá khắc khổ. Dưới đây là một vài tính năng có
thể được thêm vào:
• Giao diện quản trị máy chủ
• Hỗ trợ api Java Servlet
• Hỗ trợ cho các phương thức yêu cầu khác, chẳng hạn như POST, HEAD và PUT
• Hỗ trợ nhiều gốc tài liệu để người dùng cá nhân có thể có trang web riêng của họ
Cuối cùng, hãy dành một chút thời gian suy nghĩ về cách tối ưu hóa máy chủ này. Nếu
bạn thực sự muốn sử dụng JHTTP để chạy một trang web có lưu lượng truy cập cao, có
một vài điều có thể tăng tốc máy chủ này. Điều đầu tiên cần làm là thực hiện bộ nhớ đệm
thông minh. Theo dõi các yêu cầu bạn đã nhận được và lưu trữ dữ liệu từ các tệp được
yêu cầu thường xuyên nhất trong Bản đồ để chúng được lưu giữ trong bộ nhớ. Sử dụng
luồng ưu tiên thấp để cập nhật bộ nhớ cache này. Bạn cũng có thể thử sử dụng I/O và các
kênh không chặn thay vì luồng và luồng. Chúng ta sẽ khám phá khả năng này trong
chương 11.

You might also like