Sự khác biệt giữa read()
và recv()
, và giữa send()
và write()
trong lập trình ổ cắm về hiệu suất, tốc độ và các hành vi khác là gì?
Sự khác biệt giữa read()
và recv()
, và giữa send()
và write()
trong lập trình ổ cắm về hiệu suất, tốc độ và các hành vi khác là gì?
Câu trả lời:
Sự khác biệt là recv()
/ send()
chỉ hoạt động trên các mô tả ổ cắm và cho phép bạn chỉ định các tùy chọn nhất định cho hoạt động thực tế. Các chức năng này chuyên dụng hơn một chút (ví dụ: bạn có thể đặt cờ để bỏ quaSIGPIPE
hoặc gửi tin nhắn ngoài băng ...).
Hàm read()
/ write()
là các hàm mô tả tệp phổ quát làm việc trên tất cả các mô tả.
recv
và read
sẽ không cung cấp dữ liệu cho người gọi nhưng cũng không có lỗi. Đối với người gọi, hành vi là như nhau. Người gọi thậm chí có thể không biết gì về datagram (có thể không biết rằng đây là một socket và không phải là một tập tin, nó có thể không biết rằng đây là một socket datagram chứ không phải socket socket). Rằng datagram vẫn đang chờ xử lý là kiến thức ngầm về cách các ngăn xếp IP hoạt động trong các hạt nhân và không hiển thị cho người gọi. Từ quan điểm của người gọi, họ vẫn sẽ cung cấp hành vi bình đẳng.
recv
? Lý do tại sao recv
và send
nơi được giới thiệu ở nơi đầu tiên là thực tế là không phải tất cả các khái niệm datagram có thể được ánh xạ tới thế giới của các luồng. read
và write
coi mọi thứ là một luồng dữ liệu, cho dù đó là đường ống, tệp, thiết bị (ví dụ: cổng nối tiếp) hoặc ổ cắm. Tuy nhiên, một socket chỉ là một luồng thực nếu nó sử dụng TCP. Nếu nó sử dụng UDP, nó giống như một thiết bị khối. Nhưng nếu cả hai bên sử dụng nó như một luồng, nó sẽ hoạt động như một luồng và bạn thậm chí không thể gửi một gói UDP trống bằng write
các cuộc gọi, vì vậy tình huống này sẽ không xảy ra.
Mỗi lần truy cập đầu tiên trên Google
read () tương đương với recv () với tham số cờ là 0. Các giá trị khác cho tham số cờ thay đổi hành vi của recv (). Tương tự, write () tương đương với send () với cờ == 0.
recv
chỉ có thể được sử dụng trên một ổ cắm và sẽ gây ra lỗi nếu bạn cố gắng sử dụng nó trên, giả sử STDIN_FILENO
.
read()
và write()
chung chung hơn, chúng làm việc với bất kỳ mô tả tập tin. Tuy nhiên, chúng sẽ không hoạt động trên Windows.
Bạn có thể chuyển các tùy chọn bổ sung cho send()
và recv()
, vì vậy bạn có thể phải sử dụng chúng trong một số trường hợp.
Gần đây tôi mới nhận thấy rằng khi tôi sử dụng write()
trên một ổ cắm trong Windows, nó gần như hoạt động (FD được chuyển đến write()
không giống với cái được truyền cho send()
; tôi đã sử dụng _open_osfhandle()
để chuyển FD sang write()
). Tuy nhiên, nó không hoạt động khi tôi cố gửi dữ liệu nhị phân bao gồm ký tự 10. write()
ở đâu đó đã chèn ký tự 13 trước đó. Thay đổi nó thành send()
một tham số cờ 0 đã khắc phục vấn đề đó. read()
có thể có vấn đề ngược lại nếu 13-10 liên tiếp trong dữ liệu nhị phân, nhưng tôi chưa kiểm tra nó. Nhưng đó dường như là một sự khác biệt có thể có giữa send()
và write()
.
"Hiệu suất và tốc độ"? Không phải những loại ... từ đồng nghĩa ở đây sao?
Dù sao, recv()
cuộc gọi lấy cờ read()
không, điều đó làm cho nó mạnh hơn hoặc ít nhất là thuận tiện hơn. Đó là một sự khác biệt. Tôi không nghĩ rằng có một sự khác biệt đáng kể về hiệu suất, nhưng chưa được thử nghiệm cho nó.
Trên Linux tôi cũng nhận thấy rằng:
Ngắt các cuộc gọi hệ thống và chức năng thư viện bằng trình xử lý tín hiệu
Nếu trình xử lý tín hiệu được gọi trong khi cuộc gọi hệ thống hoặc cuộc gọi chức năng thư viện bị chặn, thì:
cuộc gọi được tự động khởi động lại sau khi bộ xử lý tín hiệu trở lại; hoặc là
cuộc gọi thất bại với lỗi EINTR.
... Các chi tiết khác nhau trên các hệ thống UNIX; bên dưới, các chi tiết cho Linux.
Nếu một cuộc gọi bị chặn đến một trong các giao diện sau bị gián đoạn bởi trình xử lý tín hiệu, thì cuộc gọi sẽ tự động được khởi động lại sau khi trình xử lý tín hiệu trả về nếu cờ SA_RESTART được sử dụng; nếu không, cuộc gọi không thành công với lỗi EINTR:
- đọc (2), readv (2), viết (2), writev (2) và ioctl (2) gọi trên các thiết bị "chậm".
.....
Các giao diện sau không bao giờ được khởi động lại sau khi bị ngắt bởi bộ xử lý tín hiệu, bất kể việc sử dụng SA_RESTART; chúng luôn bị lỗi với EINTR khi bị ngắt bởi bộ xử lý tín hiệu:
Giao diện ổ cắm "đầu vào", khi thời gian chờ (SO_RCVTIMEO) đã được đặt trên ổ cắm bằng cách sử dụng setsockopt (2): accept (2), recv (2), recvfrom (2), recvmmsg (2) (cũng không phải là NULL đối số hết thời gian chờ) và recvmsg (2).
Giao diện ổ cắm "đầu ra", khi thời gian chờ (SO_RCVTIMEO) đã được đặt trên ổ cắm bằng setsockopt (2): kết nối (2), gửi (2), sendto (2) và sendmsg (2).
Kiểm tra man 7 signal
để biết thêm chi tiết.
Một cách sử dụng đơn giản sẽ là tín hiệu sử dụng để tránh recvfrom
chặn vô thời hạn.
Một ví dụ từ APUE :
#include "apue.h"
#include <netdb.h>
#include <errno.h>
#include <sys/socket.h>
#define BUFLEN 128
#define TIMEOUT 20
void
sigalrm(int signo)
{
}
void
print_uptime(int sockfd, struct addrinfo *aip)
{
int n;
char buf[BUFLEN];
buf[0] = 0;
if (sendto(sockfd, buf, 1, 0, aip->ai_addr, aip->ai_addrlen) < 0)
err_sys("sendto error");
alarm(TIMEOUT);
//here
if ((n = recvfrom(sockfd, buf, BUFLEN, 0, NULL, NULL)) < 0) {
if (errno != EINTR)
alarm(0);
err_sys("recv error");
}
alarm(0);
write(STDOUT_FILENO, buf, n);
}
int
main(int argc, char *argv[])
{
struct addrinfo *ailist, *aip;
struct addrinfo hint;
int sockfd, err;
struct sigaction sa;
if (argc != 2)
err_quit("usage: ruptime hostname");
sa.sa_handler = sigalrm;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGALRM, &sa, NULL) < 0)
err_sys("sigaction error");
memset(&hint, 0, sizeof(hint));
hint.ai_socktype = SOCK_DGRAM;
hint.ai_canonname = NULL;
hint.ai_addr = NULL;
hint.ai_next = NULL;
if ((err = getaddrinfo(argv[1], "ruptime", &hint, &ailist)) != 0)
err_quit("getaddrinfo error: %s", gai_strerror(err));
for (aip = ailist; aip != NULL; aip = aip->ai_next) {
if ((sockfd = socket(aip->ai_family, SOCK_DGRAM, 0)) < 0) {
err = errno;
} else {
print_uptime(sockfd, aip);
exit(0);
}
}
fprintf(stderr, "can't contact %s: %s\n", argv[1], strerror(err));
exit(1);
}
#define write(...) send(##__VA_ARGS__, 0)
.