The Linux Programming Interface Chương 59 · Sockets: Internet Domains

Chương 59 · Socket miền Internet Chapter 59 · Sockets: Internet Domains

Nói chuyện qua Internet: địa chỉ IP, cổng, và cách đặt chúng lên dây Làm sao hai máy ở xa nhau nói chuyện được qua mạng Talking over the Internet: IP addresses, ports, and getting them on the wire

Sau khi Chương 56 đã giới thiệu các khái niệm socket chung, Kerrisk dành Chương 59 cho việc lập trình với socket trong hai miền Internet: IPv4 (AF_INET) và IPv6 (AF_INET6).1 Một địa chỉ socket miền Internet gồm một địa chỉ IP và một số cổng (port). Chương đi qua: thứ tự byte mạng và các hàm đổi htons()/htonl()/ntohs()/ntohl(); tính khả chuyển của biểu diễn dữ liệu; các cấu trúc địa chỉ sockaddr_in/sockaddr_in6; đổi địa chỉ giữa dạng nhị phân và dạng trình bày bằng inet_pton()/inet_ntop(); và phân giải tên host/dịch vụ bằng bộ hàm hiện đại getaddrinfo()/getnameinfo().1

Hai máy tính ở hai nơi muốn trao đổi dữ liệu qua Internet thì cần hai thứ: một địa chỉ (máy nào) và một số cổng (cửa nào trên máy đó). Chương này dạy cách viết địa chỉ và cổng ấy sao cho mọi loại máy đều hiểu giống nhau, cách tra địa chỉ từ một cái tên quen thuộc như www.example.com, và cho hai ví dụ chương trình nhỏ để hai máy gửi tin cho nhau. Nhớ nhé: nói chuyện qua mạng cần một địa chỉ máy và một số cổng.

After Chapter 56 introduced generic socket concepts, Kerrisk devotes Chapter 59 to programming with sockets in the two Internet domains: IPv4 (AF_INET) and IPv6 (AF_INET6).1 An Internet domain socket address consists of an IP address and a port number. The chapter covers: network byte order and the htons()/htonl()/ntohs()/ntohl() conversion functions; the portability of data representation; the sockaddr_in/sockaddr_in6 address structures; converting addresses between binary and presentation form with inet_pton()/inet_ntop(); and resolving host/service names with the modern getaddrinfo()/getnameinfo() functions.1

Khung cảnh lớn hơn: socket stream miền Internet được dựng trên TCP, cho một kênh truyền đáng tin cậy, hai chiều, theo luồng byte; còn socket datagram được dựng trên UDP. Khác với socket datagram miền UNIX vốn đáng tin cậy, socket UDP không đáng tin: datagram có thể bị mất, nhân đôi, hoặc tới sai thứ tự so với lúc gửi. Và khi hàng đợi của bên nhận đầy, UDP âm thầm bỏ datagram đến thay vì chặn bên gửi như socket datagram miền UNIX.1The bigger picture: Internet domain stream sockets are built on TCP, giving a reliable, bidirectional, byte-stream channel; datagram sockets are built on UDP. Unlike UNIX domain datagram sockets, which are reliable, UDP sockets are not: datagrams may be lost, duplicated, or arrive in a different order than they were sent. And when the receiver's queue is full, UDP silently drops the incoming datagram rather than blocking the sender as UNIX domain datagram sockets do.1

Luận đề của chương: để hai máy bất kỳ trên Internet hiểu nhau, dữ liệu định tuyến — địa chỉ IP và số cổng — phải được đặt vào các cấu trúc chuẩn theo một thứ tự byte thống nhất (network byte order), và con người nên làm việc với tên rồi để thư viện dịch sang số; nắm được bộ công cụ chuyển đổi và phân giải tên này là nắm được cách lập trình mạng khả chuyển.1

Ý chính: mỗi loại máy xếp byte theo kiểu riêng, nên khi gửi địa chỉ và cổng qua mạng ta phải quy về một kiểu xếp chung, và nên gõ tên dễ nhớ rồi để máy tự tra ra con số. Nhớ nhé: quy về một thứ tự byte chung, và để thư viện dịch tên thành số.

The chapter's thesis: for any two machines on the Internet to understand each other, routing data — IP addresses and port numbers — must be placed into standard structures in a unified byte order (network byte order), and humans should work with names and let libraries translate them to numbers; mastering this conversion-and-resolution toolkit is mastering portable network programming.1

Cụm khái niệm 01 Concept cluster 01

Các máy xếp byte khác nhau — nên số trên dây phải dùng một thứ tự chung Mỗi máy xếp byte một kiểu, nên phải quy về một kiểu chung khi gửi Machines order bytes differently — so numbers on the wire need a common order

Địa chỉ IP và số cổng là các số nguyên. Khi truyền chúng qua mạng ta gặp một trở ngại: các kiến trúc phần cứng khác nhau lưu các byte của một số nguyên nhiều byte theo thứ tự khác nhau. Kiến trúc lưu byte có nghĩa lớn nhất trước (ở địa chỉ nhớ thấp nhất) gọi là big-endian; kiến trúc lưu byte có nghĩa nhỏ nhất trước gọi là little-endian.1 Thứ tự byte dùng trên một máy cụ thể gọi là host byte order (thứ tự byte của máy). Vì không thể biết trước máy bên kia thuộc kiểu nào, các giá trị số định tuyến được quy ước đặt trên dây theo một chuẩn gọi là network byte order — và chuẩn đó là big-endian.12

Tưởng tượng số 258 phải xếp vào hai ô nhớ. Máy kiểu này viết "1 rồi 2", máy kiểu kia viết "2 rồi 1" — cùng một số nhưng thứ tự byte ngược nhau. Nếu cứ gửi thô, bên nhận có thể đọc nhầm thành số khác. Nên trước khi gửi, ta luôn quy mọi con số về một kiểu xếp chung mà cả mạng đồng ý. Nhớ nhé: trước khi gửi số qua mạng, đổi nó sang thứ tự byte chuẩn của mạng.

IP addresses and port numbers are integer values. When passing them across a network we hit an obstacle: different hardware architectures store the bytes of a multibyte integer in different orders. Architectures that store the most significant byte first (at the lowest memory address) are termed big-endian; those that store the least significant byte first are termed little-endian.1 The byte ordering used on a particular machine is called the host byte order. Since you cannot know in advance which kind the far machine is, routing integers are by convention placed on the wire in a standard called network byte order — and that standard is big-endian.12

Định nghĩa. Bốn hàm đổi thứ tự byte (thường cài đặt dưới dạng macro) chuyển qua lại giữa host byte order và network byte order: htons()/htonl() đổi từ host sang network (cho số 16-bit và 32-bit), ntohs()/ntohl() đổi theo chiều ngược lại. Chữ "s" là short (16-bit), "l" là long (32-bit).12

Nói đơn giản. Có bốn cái "máy phiên dịch byte": hai cái đổi từ kiểu của máy mình sang kiểu của mạng, hai cái đổi ngược lại. Dùng chúng là xong, không cần tự biết máy mình kiểu gì. Nhớ nhé: htons/htonl để gửi đi, ntohs/ntohl để nhận về.

Definition. Four byte-order conversion functions (typically implemented as macros) translate between host and network byte order: htons()/htonl() convert from host to network (for 16-bit and 32-bit values), and ntohs()/ntohl() convert the other way. The "s" is short (16-bit), the "l" is long (32-bit).12

#include <arpa/inet.h>
uint16_t htons(uint16_t host_uint16);
uint32_t htonl(uint32_t host_uint32);
uint16_t ntohs(uint16_t net_uint16);
uint32_t ntohl(uint32_t net_uint32);
/* h=host n=net s=16-bit l=32-bit */

htons() --> cổng 16-bit đi gửi
htonl() --> địa chỉ 32-bit đi gửi
ntohs() --> nhận cổng về
ntohl() --> nhận địa chỉ về

#include <arpa/inet.h>
uint16_t htons(uint16_t host_uint16);
uint32_t htonl(uint32_t host_uint32);
uint16_t ntohs(uint16_t net_uint16);
uint32_t ntohl(uint32_t net_uint32);
/* h=host n=net s=16-bit l=32-bit */

  1. big vs little endian

    Hai phe trứng luộc: cái tên đến từ Gulliver's Travels Hai kiểu xếp byte, tên lấy từ chuyện bổ trứng Two boiled-egg factions: the names come from Gulliver's Travels

    Kerrisk ghi rõ nguồn gốc thuật ngữ: "big-endian" và "little-endian" bắt nguồn từ tiểu thuyết trào phúng Gulliver's Travels của Jonathan Swift năm 1726, nơi hai từ này chỉ hai phe chính trị đối nghịch — một bên đập vỏ trứng luộc ở đầu to, bên kia ở đầu nhỏ.1 Ví dụ nổi bật nhất của kiến trúc little-endian là x86; phần lớn các kiến trúc khác là big-endian, và một số ít chuyển được giữa hai kiểu.1

    Hai cái tên nghe lạ này thực ra lấy từ một cuốn truyện năm 1726, nơi hai phe cãi nhau nên đập quả trứng luộc ở đầu to hay đầu nhỏ. Máy tính cũng vậy: có máy xếp byte "đầu to trước", có máy "đầu nhỏ trước". Máy để bàn x86 quen thuộc thuộc phe đầu nhỏ. Nhớ nhé: tên nghe buồn cười nhưng chỉ nói về thứ tự xếp byte.

    Kerrisk notes the origin of the terms: "big-endian" and "little-endian" derive from Jonathan Swift's 1726 satirical novel Gulliver's Travels, in which they refer to opposing political factions who open their boiled eggs at opposite ends.1 The most notable little-endian architecture is x86; most other architectures are big-endian, and a few are switchable between the two.1

    Bạn gặp nó ở đâuWhere you meet it Khi một chương trình mạng viết trên x86 (little-endian) chạy đúng trên máy khác — đó là vì nó đã đổi mọi cổng và địa chỉ sang network byte order, chứ không dựa vào thứ tự byte của chính nó.1 Khi một chương trình chạy đúng trên nhiều loại máy khác nhau — vì nó đã quy số về kiểu chung của mạng. When a network program written on x86 (little-endian) runs correctly on a different machine — it is because it converted every port and address to network byte order rather than trusting its own byte order.1

    Vì sao vẫn gọi dù máy đã big-endianWhy call them even on a big-endian host

    Network byte order chính là big-endian, nên trên một máy big-endian các hàm này thực chất không đổi gì. Nhưng ta vẫn luôn gọi chúng: nhờ đó cùng một mã nguồn chạy đúng trên cả hai loại máy mà không cần biết trước host byte order.12Network byte order is big-endian, so on a big-endian host these functions are effectively no-ops. But you still call them always: that way the same source runs correctly on either kind of machine without having to know the host byte order in advance.12

    Kiểu dữ liệuThe data types

    Các kiểu uint16_t và uint32_t là số nguyên không dấu 16-bit và 32-bit. Cổng là 16-bit (nên dùng htons), địa chỉ IPv4 là 32-bit (nên dùng htonl).1The uint16_t and uint32_t types are 16-bit and 32-bit unsigned integers. A port is 16-bit (hence htons), an IPv4 address is 32-bit (hence htonl).1

  2. data representation

    Byte order mới là khởi đầu: còn cả bài toán biểu diễn dữ liệu Không chỉ địa chỉ — gửi dữ liệu nào cũng phải thống nhất cách đọc Byte order is just the start: there's a whole data-representation problem

    Thứ tự byte chỉ là một mặt của bài toán lớn hơn: biểu diễn dữ liệu khả chuyển. Hai máy nối với nhau có thể khác nhau không chỉ về endianness mà còn về kích thước kiểu dữ liệu, cách căn lề, hay cách mã hoá ký tự. Kerrisk khuyến nghị cách đơn giản và bền vững nhất khi trao đổi dữ liệu giữa các máy khác kiến trúc: mã hoá thông tin dưới dạng văn bản (ví dụ các dòng kết thúc bằng ký tự xuống dòng), thay vì gửi thẳng cấu trúc nhị phân trong bộ nhớ.1

    Ngay cả khi đã lo xong thứ tự byte, hai máy vẫn có thể hiểu nhầm nhau nếu một bên gửi "cục dữ liệu thô" trong bộ nhớ: kích thước kiểu số, cách căn chữ có thể khác. Cách chắc ăn là gửi dưới dạng chữ — như viết thư — rồi mỗi bên tự diễn giải. Nhớ nhé: muốn chắc ăn giữa hai máy lạ nhau, hãy gửi dạng văn bản.

    Byte order is one face of a larger problem: portable data representation. Two connected machines may differ not only in endianness but in data-type sizes, alignment, or character encoding. Kerrisk recommends the simplest, most durable approach for exchanging data between differing architectures: encode information as text (for example, newline-terminated lines), rather than sending raw in-memory binary structures.1

    Bạn gặp nó ở đâuWhere you meet it Mọi giao thức Internet dạng văn bản — HTTP, SMTP, FTP — đều là hiện thân của lời khuyên này: trao đổi bằng dòng chữ dễ chẩn đoán và khả chuyển, không phụ thuộc kiến trúc máy.1 Các giao thức quen như HTTP, email đều gửi bằng chữ — chính là vì lý do này. Every text-based Internet protocol — HTTP, SMTP, FTP — embodies this advice: exchanging lines of text is diagnosable and portable, independent of machine architecture.1
  3. why ports use htons

    Vì sao phải gọi htons cho từng số cổng bạn đặt Đặt số cổng vào địa chỉ thì luôn phải đổi qua htons Why every port number you set must go through htons

    Khi điền số cổng vào trường sin_port của một cấu trúc địa chỉ, bạn phải bọc nó bằng htons(), vì mọi trường địa chỉ và cổng trong các cấu trúc socket đều lưu ở network byte order.1 Quên bước này là lỗi kinh điển của người mới: trên x86, cổng 80 (0x0050) không đổi sẽ bị đọc thành cổng 20480 (0x5000) ở phía bên kia.12

    Khi bạn ghi "tôi muốn cổng 80" vào hộp địa chỉ, phải ghi qua htons(80) chứ không ghi thẳng số 80, vì hộp đó yêu cầu kiểu xếp byte của mạng. Quên đổi là bên kia hiểu thành một cổng hoàn toàn khác. Nhớ nhé: điền cổng vào địa chỉ thì luôn bọc htons.

    When you fill a port number into a structure's sin_port field, you must wrap it in htons(), because all address and port fields in socket structures are stored in network byte order.1 Forgetting this is a classic beginner bug: on x86, port 80 (0x0050) left unconverted will be read as port 20480 (0x5000) at the far end.12

    Bạn gặp nó ở đâuWhere you meet it Khi một server "nghe" đúng cổng trên máy mình nhưng client không bao giờ kết nối được — rất thường là vì một bên quên htons() trên số cổng. Khi server tưởng nghe đúng cổng mà client gọi mãi không tới — hay quên đổi cổng qua htons. When a server seems to listen on the right port but clients never connect — it is very often a forgotten htons() on the port number.

Cụm khái niệm 02 Concept cluster 02

Chiếc hộp chứa địa chỉ: sockaddr_in, sockaddr_in6, và cách viết/đọc địa chỉ Hộp đựng địa chỉ cho IPv4 và IPv6, và cách điền địa chỉ vào The address box: sockaddr_in, sockaddr_in6, and reading/writing addresses

Một địa chỉ socket IPv4 được lưu trong cấu trúc sockaddr_in (định nghĩa trong <netinet/in.h>), gồm ba trường chính: sin_family luôn đặt là AF_INET, sin_port là số cổng, và sin_addr là địa chỉ IPv4 — bản thân là một cấu trúc in_addr chứa một số nguyên 32-bit không dấu s_addr.15 Địa chỉ IPv6 dùng sockaddr_in6 với sin6_family = AF_INET6, sin6_port, và sin6_addr (một in6_addr chứa địa chỉ 128-bit, tức 16 byte).16 Mọi trường cổng và địa chỉ trong các cấu trúc này đều ở network byte order.1

Để gửi gói tin đi, ta cần một "chiếc hộp" ghi rõ: đây là loại địa chỉ gì (IPv4 hay IPv6), cổng số mấy, và địa chỉ máy là gì. IPv4 dùng hộp tên sockaddr_in, IPv6 dùng hộp lớn hơn sockaddr_in6 vì địa chỉ IPv6 dài hơn nhiều. Nhớ nhé: mỗi địa chỉ nằm trong một chiếc hộp chuẩn, ghi rõ loại, cổng và địa chỉ.

An IPv4 socket address is stored in a sockaddr_in structure (defined in <netinet/in.h>) with three main fields: sin_family, always set to AF_INET; sin_port, the port number; and sin_addr, the IPv4 address — itself an in_addr structure holding a 32-bit unsigned integer s_addr.15 An IPv6 address uses sockaddr_in6 with sin6_family = AF_INET6, sin6_port, and sin6_addr (an in6_addr holding a 128-bit, i.e. 16-byte, address).16 Every port and address field in these structures is in network byte order.1

Định nghĩa. Dạng trình bày (presentation) là địa chỉ con người đọc được — IPv4 dạng thập phân chấm "204.152.189.116", IPv6 dạng hex-colon "::1". Dạng nhị phân (binary) là chuỗi byte số mà cấu trúc địa chỉ thực sự lưu. inet_pton() đổi presentation → binary (chữ p–to–n), còn inet_ntop() đổi binary → presentation.134

Nói đơn giản. Con người thích viết địa chỉ kiểu "192.168.0.1"; máy lại cần nó ở dạng mấy con số byte. Hai hàm inet_pton và inet_ntop là cầu nối hai chiều giữa "chữ người đọc" và "số máy hiểu". Nhớ nhé: pton biến chữ thành số, ntop biến số thành chữ.

Definition. Presentation form is the human-readable address — IPv4 dotted-decimal "204.152.189.116", IPv6 hex-colon "::1". Binary form is the numeric byte sequence the address structure actually holds. inet_pton() converts presentation → binary (p–to–n), while inet_ntop() converts binary → presentation.134

#include <arpa/inet.h>
int inet_pton(int domain,
  const char *src_str, void *addrptr);
const char *inet_ntop(int domain,
  const void *addrptr,
  char *dst_str, size_t len);
/* domain = AF_INET | AF_INET6 */

inet_pton("1.2.3.4") --> số
inet_ntop(số) --> "1.2.3.4"
(domain: AF_INET hoặc AF_INET6)

#include <arpa/inet.h>
int inet_pton(int domain,
  const char *src_str, void *addrptr);
const char *inet_ntop(int domain,
  const void *addrptr,
  char *dst_str, size_t len);
/* domain = AF_INET | AF_INET6 */

  1. sockaddr_in / in_addr

    Cấu trúc IPv4 và vì sao mọi API socket nhận sockaddr chung Hộp địa chỉ IPv4, và vì sao phải ép kiểu khi gọi hàm The IPv4 structure and why every socket API takes a generic sockaddr

    Các system call như bind(), connect() hay accept() nhận một con trỏ tới kiểu chung struct sockaddr, chứ không nhận trực tiếp sockaddr_in. Vì vậy, khi gọi, ta điền một sockaddr_in cụ thể rồi ép kiểu con trỏ về (struct sockaddr *) và truyền kèm độ dài sizeof(struct sockaddr_in).15 Cơ chế này cho phép cùng một bộ API phục vụ mọi miền địa chỉ — IPv4, IPv6, UNIX — mà không cần hàm riêng cho từng loại.1

    Các hàm mạng không đòi đúng một loại hộp, mà đòi một "hộp tổng quát". Nên ta cứ điền hộp IPv4 của mình, rồi dán nhãn "đây là hộp tổng quát" (ép kiểu) khi trao cho hàm. Nhờ vậy cùng một hàm dùng được cho mọi loại địa chỉ. Nhớ nhé: điền hộp cụ thể, rồi ép kiểu về sockaddr chung khi gọi.

    System calls such as bind(), connect(), and accept() take a pointer to the generic struct sockaddr, not a sockaddr_in directly. So when calling, you fill a concrete sockaddr_in, cast the pointer to (struct sockaddr *), and pass the length sizeof(struct sockaddr_in).15 This lets one API serve every address domain — IPv4, IPv6, UNIX — without a separate function per type.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi lần bạn thấy bind(fd, (struct sockaddr *) &addr, sizeof(addr)) — phép ép kiểu đó chính là cơ chế địa chỉ chung đang làm việc.5 Khi thấy đoạn mã ép kiểu địa chỉ lúc gọi bind — đó là vì hàm đòi hộp tổng quát. Every time you see bind(fd, (struct sockaddr *) &addr, sizeof(addr)) — that cast is the generic-address mechanism at work.5

    Địa chỉ wildcard và loopbackWildcard and loopback addresses

    IPv4 có địa chỉ wildcard INADDR_ANY (để server nghe trên mọi giao diện mạng của máy) và địa chỉ loopback INADDR_LOOPBACK (127.0.0.1, trỏ về chính máy). IPv6 có bản tương ứng: wildcard in6addr_any (viết 0::0) và loopback in6addr_loopback (::1).16IPv4 has a wildcard address INADDR_ANY (for a server to listen on all of the host's network interfaces) and a loopback address INADDR_LOOPBACK (127.0.0.1, back to the same host). IPv6 has the equivalents: wildcard in6addr_any (written 0::0) and loopback in6addr_loopback (::1).16

    sockaddr_in6 có thêm hai trườngsockaddr_in6 has two extra fields

    Ngoài family/port/addr, sockaddr_in6 còn có sin6_flowinfo và sin6_scope_id (trường thứ hai mới có từ nhân 2.4). Kerrisk nói hai trường này nằm ngoài phạm vi sách và với mục đích của ta thì luôn đặt về 0.16Besides family/port/addr, sockaddr_in6 also has sin6_flowinfo and sin6_scope_id (the latter new in kernel 2.4). Kerrisk says both are beyond the book's scope and, for our purposes, are always set to 0.16

  2. inet_pton / inet_ntop

    Đổi địa chỉ giữa chữ và số — và nhớ cấp đủ chỗ Hai hàm đổi địa chỉ chữ ↔ số, cho cả IPv4 lẫn IPv6 Convert addresses between text and binary — with a big enough buffer

    inet_pton() nhận domain (AF_INET hoặc AF_INET6), đọc chuỗi src_str và ghi dạng nhị phân vào addrptr; nó trả về 1 nếu thành công, 0 nếu chuỗi không hợp lệ, −1 nếu lỗi. inet_ntop() làm chiều ngược, ghi chuỗi vào vùng đệm dst_str dài len và trả về dst_str — nhưng nếu len quá nhỏ thì thất bại.134 Để cấp đủ chỗ, dùng hằng INET_ADDRSTRLEN (16) cho IPv4 và INET6_ADDRSTRLEN (46) cho IPv6.14

    Muốn biến "10.0.0.1" thành số thì gọi inet_pton; muốn in một địa chỉ số ra cho người đọc thì gọi inet_ntop. Khi in ra, nhớ chuẩn bị một ô chứa đủ rộng — sách cho sẵn hai con số: 16 cho IPv4, 46 cho IPv6. Nhớ nhé: in địa chỉ ra chữ thì chừa đủ 46 ký tự cho chắc.

    inet_pton() takes a domain (AF_INET or AF_INET6), reads the string src_str, and writes binary form into addrptr; it returns 1 on success, 0 if the string is not valid, −1 on error. inet_ntop() goes the other way, writing into the dst_str buffer of length len and returning dst_str — but it fails if len is too small.134 To size the buffer, use the constant INET_ADDRSTRLEN (16) for IPv4 and INET6_ADDRSTRLEN (46) for IPv6.14

    Bạn gặp nó ở đâuWhere you meet it Ví dụ server trong sách dùng inet_ntop() để in địa chỉ của client vừa kết nối ra một chuỗi dễ đọc cho log — đúng kiểu dùng thực tế của hàm này.1 Khi server ghi log "có client từ địa chỉ X vừa kết nối" — chính inet_ntop đã in địa chỉ đó ra chữ. The book's server uses inet_ntop() to print a connecting client's address as a readable string for logging — exactly this function's practical use.1
  3. inet_aton / inet_ntoa

    Hai hàm cũ chỉ cho IPv4 — và vì sao nên bỏ chúng Hai hàm địa chỉ đời cũ chỉ làm được IPv4 The older IPv4-only pair — and why to leave them behind

    Trước inet_pton/inet_ntop, có cặp cũ hơn inet_aton() và inet_ntoa() cũng đổi địa chỉ IPv4 giữa dạng thập phân chấm và dạng nhị phân. Khác biệt mấu chốt: cặp mới làm được cả IPv4 lẫn IPv6, còn cặp cũ chỉ xử lý được IPv4.17 Vì thế Kerrisk xếp cặp cũ vào nhóm API lỗi thời (thảo luận ở phần 59.13), và mã mới nên dùng cặp inet_pton/inet_ntop.1

    Ngày xưa có hai hàm inet_aton và inet_ntoa làm việc tương tự nhưng chỉ cho địa chỉ IPv4. Đời sau cần cả IPv6, nên có cặp mới làm được cả hai. Nhớ nhé: viết mã mới thì dùng inet_pton/inet_ntop, bỏ cặp cũ chỉ-IPv4.

    Before inet_pton/inet_ntop, the older pair inet_aton() and inet_ntoa() also converted IPv4 addresses between dotted-decimal and binary. The key difference: the newer pair handles both IPv4 and IPv6, while the older pair handles only IPv4.17 Kerrisk therefore groups the old pair with the obsolete APIs (discussed in §59.13), and new code should use inet_pton/inet_ntop.1

    Bạn gặp nó ở đâuWhere you meet it Khi đọc mã nguồn cũ thấy inet_ntoa() trả thẳng một chuỗi — tiện nhưng nó dùng vùng đệm tĩnh và chỉ IPv4, nên mã mới nên thay bằng inet_ntop().7 Thấy mã cũ gọi inet_ntoa — chạy được, nhưng nên nâng cấp lên hàm mới. When old code uses inet_ntoa() returning a string directly — handy, but it uses a static buffer and is IPv4-only, so new code should prefer inet_ntop().7

Cụm khái niệm 03 Concept cluster 03

Từ tên tới địa chỉ: getaddrinfo, getnameinfo, và hai ví dụ client-server Đổi tên thành địa chỉ, và hai chương trình nhỏ cho hai máy nói chuyện From names to addresses: getaddrinfo, getnameinfo, and two client-server examples

getaddrinfo() là hàm kế tục hiện đại của cả gethostbyname() lẫn getservbyname(): cho một tên host và một tên dịch vụ, nó trả về một tập các cấu trúc địa chỉ socket, sẵn sàng truyền cho bind() hay connect(). Khác hàm cũ, nó xử lý trong suốt cả IPv4 lẫn IPv6, nên che được mã của ta khỏi chi tiết phiên bản IP.1711 getnameinfo() làm phép dịch ngược: từ một cấu trúc địa chỉ socket ra tên host và tên dịch vụ.18 Sách còn mô tả DNS — cơ sở dữ liệu phân tán ánh xạ tên host ↔ địa chỉ IP — và tệp /etc/services cho tên dịch vụ.1

Khi bạn gõ một tên trang web, máy phải tra ra địa chỉ số của nó. Hàm getaddrinfo làm đúng việc đó: đưa vào một cái tên (và tên dịch vụ như http), nhận về địa chỉ và cổng sẵn dùng. Nó lo luôn cả IPv4 và IPv6 nên bạn không phải bận tâm. Hàm getnameinfo làm điều ngược lại: từ địa chỉ số tìm về cái tên. Nhớ nhé: getaddrinfo đổi tên thành địa chỉ, getnameinfo đổi địa chỉ thành tên.

getaddrinfo() is the modern successor to both gethostbyname() and getservbyname(): given a host name and a service name, it returns a set of socket address structures ready to pass to bind() or connect(). Unlike the old functions, it transparently handles both IPv4 and IPv6, shielding your code from the IP version in use.1711 getnameinfo() performs the reverse translation: from a socket address structure to a host name and service name.18 The book also describes DNS — a distributed database mapping host names ↔ IP addresses — and the /etc/services file for service names.1

Định nghĩa. getaddrinfo() cấp phát một danh sách liên kết các cấu trúc addrinfo (trả qua tham số result); mỗi nút mang một địa chỉ ứng viên. Sau khi dùng xong, phải giải phóng danh sách bằng freeaddrinfo(). Khi lỗi, hàm trả về mã lỗi khác 0 mà gai_strerror() dịch ra chuỗi tiếng người đọc được.17

Nói đơn giản. getaddrinfo đưa về một xâu các địa chỉ có thể dùng (vì một tên có thể ứng với nhiều địa chỉ). Dùng xong thì gọi freeaddrinfo để trả lại bộ nhớ, như dọn bàn sau khi ăn. Nhớ nhé: xin địa chỉ bằng getaddrinfo, dùng xong nhớ freeaddrinfo.

Definition. getaddrinfo() allocates a linked list of addrinfo structures (returned via the result argument); each node carries a candidate address. When done, you must free the list with freeaddrinfo(). On failure it returns a nonzero error code that gai_strerror() translates into a human-readable string.17

#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host,
  const char *service,
  const struct addrinfo *hints,
  struct addrinfo **result);
void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);

getaddrinfo(tên, dịch_vụ, ...)
  --> xâu địa chỉ (result)
freeaddrinfo(result) --> dọn
gai_strerror(mã) --> câu lỗi

#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *host,
  const char *service,
  const struct addrinfo *hints,
  struct addrinfo **result);
void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);

  1. getaddrinfo + hints

    Duyệt danh sách địa chỉ, và dùng hints để ra điều kiện Thử lần lượt các địa chỉ trả về cho tới khi kết nối được Walk the address list, and steer it with hints

    Mẫu dùng điển hình: điền một cấu trúc hints để ra điều kiện (ví dụ ai_family = AF_UNSPEC cho phép cả IPv4 lẫn IPv6; ai_socktype = SOCK_STREAM để tra dịch vụ TCP), gọi getaddrinfo(), rồi duyệt lần lượt danh sách result: với mỗi nút, thử socket() rồi connect() (client) hoặc bind() (server), dừng ở địa chỉ đầu tiên thành công.17 Server dùng thêm cờ AI_PASSIVE để lấy địa chỉ wildcard.1

    Một cái tên có thể ứng với nhiều địa chỉ. Nên cách làm là: xin cả xâu, rồi thử từng địa chỉ cho tới khi có một cái kết nối được thì dừng. Ta còn đưa vào vài "gợi ý" để nói rõ mình muốn kiểu kết nối nào. Nhớ nhé: thử lần lượt các địa chỉ cho tới khi được.

    The typical pattern: fill a hints structure to constrain the query (e.g. ai_family = AF_UNSPEC to allow IPv4 or IPv6; ai_socktype = SOCK_STREAM to look up a TCP service), call getaddrinfo(), then walk the result list: for each node, try socket() then connect() (client) or bind() (server), stopping at the first address that succeeds.17 A server also sets the AI_PASSIVE flag to obtain a wildcard address.1

    Bạn gặp nó ở đâuWhere you meet it Mẫu "vòng lặp duyệt result" này xuất hiện trong gần như mọi client/server TCP hiện đại — kể cả thư viện socket tiện ích mà Kerrisk xây ở phần 59.12 để gói lại các bước lặp đi lặp lại.1 Vòng lặp "thử từng địa chỉ" này có mặt trong hầu hết chương trình mạng ngày nay. This "loop over result" pattern appears in nearly every modern TCP client/server — including the convenience socket library Kerrisk builds in §59.12 to wrap the repetitive steps.1

    getnameinfo và các cờgetnameinfo and its flags

    getnameinfo() nhận một địa chỉ socket và điền tên host vào đệm dài NI_MAXHOST, tên dịch vụ vào đệm dài NI_MAXSERV. Các cờ như NI_NUMERICHOST/NI_NUMERICSERV buộc trả về dạng số thay vì tra tên; NI_DGRAM buộc lấy tên dịch vụ UDP khi tên TCP và UDP khác nhau.18getnameinfo() takes a socket address and fills a host name into a buffer of length NI_MAXHOST and a service name into one of length NI_MAXSERV. Flags such as NI_NUMERICHOST/NI_NUMERICSERV force numeric output instead of a name lookup; NI_DGRAM forces the UDP service name when the TCP and UDP names differ.18

    Dùng cho cả hai chiều số–chữFor both numeric–text directions

    Kerrisk lưu ý có thể dùng chính getaddrinfo()/getnameinfo() để đổi địa chỉ nhị phân ↔ dạng trình bày, thay cho inet_pton/inet_ntop — nhờ các cờ "numeric" ép bỏ qua bước tra DNS/dịch vụ.18Kerrisk notes you can use getaddrinfo()/getnameinfo() themselves to convert binary ↔ presentation addresses, in place of inet_pton/inet_ntop — the "numeric" flags force them to skip the DNS/service lookup.18

  2. /etc/services + DNS

    Cổng theo tên nằm ở /etc/services; tên host nằm trong DNS Tên dịch vụ tra ở một tệp; tên máy tra qua DNS Service-by-name lives in /etc/services; host names live in DNS

    Ánh xạ tên dịch vụ ↔ số cổng được ghi trong tệp /etc/services, gồm các dòng ba cột (tên dịch vụ, số cổng/giao thức, bí danh). Kerrisk nhấn mạnh một điểm hay bị hiểu nhầm: tệp này chỉ là bản ghi ánh xạ tên–số, không phải cơ chế đặt chỗ — việc một cổng xuất hiện trong tệp không ngăn tiến trình khác dùng cổng đó.110 Tên host thì được tra qua DNS, một cơ sở dữ liệu phân tán; chương trình gọi tới bộ "resolver" để biến tên miền thành địa chỉ.1

    Tên dịch vụ như http hay ssh ứng với cổng nào thì tra trong tệp /etc/services trên máy. Còn tên máy như example.com thì phải hỏi hệ thống DNS — giống như một cuốn danh bạ khổng lồ của cả Internet, chia nhỏ ra khắp nơi. Nhớ nhé: cổng tra ở tệp trong máy, tên máy hỏi DNS ngoài mạng.

    The service-name ↔ port-number mapping is recorded in /etc/services, lines of three columns (service name, port/protocol, aliases). Kerrisk stresses a commonly misunderstood point: this file is merely a record of name-to-number mappings, not a reservation mechanism — a port appearing in the file does not stop another process from using it.110 Host names are resolved through DNS, a distributed database; a program calls the "resolver" to turn a domain name into an address.1

    Bạn gặp nó ở đâuWhere you meet it Khi gọi getaddrinfo(host, "http", ...) và nhận được cổng 80 — chính /etc/services đã cung cấp con số đó cho tên http.10 Khi dùng tên http mà ra cổng 80 — con số ấy lấy từ tệp /etc/services. When you call getaddrinfo(host, "http", ...) and get port 80 — it was /etc/services that supplied that number for the name http.10
  3. TCP vs UDP examples

    Hai ví dụ: client-server stream (TCP) và datagram (UDP) Hai kiểu ví dụ: một kiểu đáng tin, một kiểu nhanh mà có thể mất Two examples: stream (TCP) and datagram (UDP) client-server

    Chương cho hai ví dụ client-server song song. Ví dụ stream (phần 59.11) dùng socket TCP: một server lặp (iterative) phục vụ lần lượt từng client qua kênh byte đáng tin cậy. Ví dụ datagram (phần 59.7) dùng socket UDP, trong đó server dùng inet_ntop() để in địa chỉ client và gắn vào địa chỉ wildcard in6addr_any; client thì không cần bind() địa chỉ của mình.1 Điểm cốt lõi khi chọn: TCP đáng tin cậy và theo luồng, còn UDP nhanh nhưng datagram có thể mất, nhân đôi hoặc tới sai thứ tự.1

    Sách cho hai ví dụ. Một kiểu (TCP) giống gọi điện thoại: nối máy xong mới nói, lời nói tới đủ và đúng thứ tự. Kiểu kia (UDP) giống gửi bưu thiếp: ném đi là xong, nhanh nhưng có thể lạc hoặc tới lộn xộn. Tuỳ việc mà chọn. Nhớ nhé: TCP như gọi điện (chắc), UDP như gửi bưu thiếp (nhanh mà có thể lạc).

    The chapter gives two parallel client-server examples. The stream example (§59.11) uses TCP sockets: an iterative server serves clients one at a time over a reliable byte channel. The datagram example (§59.7) uses UDP, where the server uses inet_ntop() to print the client's address and binds to the wildcard in6addr_any; the client does not need to bind() its own address.1 The core trade-off: TCP is reliable and stream-oriented, while UDP is fast but datagrams may be lost, duplicated, or arrive out of order.1

    Bạn gặp nó ở đâuWhere you meet it Web, email, SSH chọn TCP vì cần dữ liệu toàn vẹn và đúng thứ tự; còn DNS, truyền video trực tiếp hay game thường chọn UDP vì ưu tiên tốc độ và chấp nhận mất mát nhỏ.1 Web và email dùng TCP cho chắc; còn gọi video, game hay chọn UDP cho nhanh. Web, email, and SSH choose TCP for intact, in-order data; DNS, live video, and games often choose UDP, favoring speed and tolerating small losses.1

Ba hiểu lầm thường gặp khi lập trình socket Internet Three common misconceptions in Internet socket programming

"Máy mình big-endian rồi, khỏi cần htons" "My host is big-endian, so I can skip htons"

Hiểu lầm: nếu máy đã dùng big-endian giống network byte order thì gọi htons()/htonl() là thừa.

Thực tế: trên máy big-endian các hàm này không đổi gì, nhưng vẫn phải luôn gọi — bỏ đi là mã của bạn sẽ hỏng ngay khi biên dịch lại trên một máy little-endian như x86.12

Myth: if the host is already big-endian like network byte order, calling htons()/htonl() is redundant.

Reality: on a big-endian host they are no-ops, but you must always call them — dropping them breaks your code the moment it is recompiled on a little-endian machine such as x86.12

"Cứ ghi thẳng struct nhị phân ra là gửi được" "Just dump the binary struct onto the wire"

Hiểu lầm: gửi nguyên một cấu trúc trong bộ nhớ qua mạng là cách nhanh và an toàn để trao đổi dữ liệu.

Thực tế: endianness, kích thước kiểu và cách căn lề khác nhau giữa các máy làm cục nhị phân thô dễ bị đọc sai; Kerrisk khuyên mã hoá dữ liệu dạng văn bản để khả chuyển.1

Myth: sending a whole in-memory structure across the network is a fast, safe way to exchange data.

Reality: differing endianness, type sizes, and alignment across machines make raw binary blobs easy to misread; Kerrisk advises encoding data as text for portability.1

"gethostbyname vẫn tốt, dùng cho mã mới cũng được" "gethostbyname is fine for new code"

Hiểu lầm: gethostbyname()/getservbyname() quen thuộc nên cứ dùng cho chương trình mới.

Thực tế: chúng là IPv4-centric và đã lỗi thời — SUSv4 loại bỏ đặc tả của chúng; mã mới nên dùng getaddrinfo()/getnameinfo() vốn xử lý cả IPv4 lẫn IPv6.19

Myth: gethostbyname()/getservbyname() are familiar, so keep using them for new programs.

Reality: they are IPv4-centric and obsolete — SUSv4 removes their specifications; new code should use getaddrinfo()/getnameinfo(), which handle both IPv4 and IPv6.19

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm socket miền Internet Năm câu hỏi kiểm tra nhanh Five questions to check you grasped Internet domain sockets

Trả lời được những câu này nghĩa là bạn đã nắm cách đặt địa chỉ và cổng lên dây một cách khả chuyển, và cách để chương trình làm việc với tên thay vì số.

Nếu trả lời được, nghĩa là bạn đã hiểu chương này.

Answering these means you grasp how to put addresses and ports on the wire portably, and how to let programs work with names instead of numbers.

  1. Big-endian khác little-endian ở điểm nào? Vì sao phải đổi cổng và địa chỉ sang network byte order trước khi gửi?

    Gợi ý sâu hơn: trên một máy big-endian, bốn hàm htons/htonl/ntohs/ntohl làm gì, và vì sao vẫn phải gọi chúng?

  2. Kể ba trường chính của sockaddr_in. Vì sao các API như bind() lại nhận con trỏ struct sockaddr chung thay vì sockaddr_in?

    Gợi ý sâu hơn: sockaddr_in6 có thêm hai trường nào, và Kerrisk nói đặt chúng về giá trị gì cho mục đích của sách?

  3. inet_pton() và inet_ntop() đổi địa chỉ theo chiều nào? Vì sao chúng tốt hơn cặp cũ inet_aton()/inet_ntoa()?

    Gợi ý sâu hơn: INET_ADDRSTRLEN và INET6_ADDRSTRLEN bằng bao nhiêu, và dùng để làm gì?

  4. getaddrinfo() trả về cái gì, và vì sao phải duyệt lần lượt rồi gọi freeaddrinfo()? Nó thay thế những hàm cũ nào?

    Gợi ý sâu hơn: cờ AI_PASSIVE dùng cho server để làm gì, và getnameinfo() làm phép dịch theo chiều nào?

  5. Khác nhau giữa socket stream (TCP) và datagram (UDP) miền Internet là gì? Tên dịch vụ tra ở đâu, tên host tra ở đâu?

    Gợi ý sâu hơn: vì sao Kerrisk nhấn mạnh /etc/services không phải cơ chế đặt chỗ cổng?

  1. Vì sao gửi một con số qua mạng phải đổi "thứ tự byte" trước?

  2. "Chiếc hộp" chứa địa chỉ cần ghi những gì?

  3. Hai hàm nào đổi địa chỉ giữa dạng chữ và dạng số?

  4. Làm sao máy biết địa chỉ của một tên như example.com?

  5. TCP và UDP khác nhau ra sao, ví như gọi điện và gửi bưu thiếp?

  1. How does big-endian differ from little-endian? Why convert ports and addresses to network byte order before sending?

  2. Name the three main fields of sockaddr_in. Why do APIs like bind() take a generic struct sockaddr pointer rather than a sockaddr_in?

  3. Which way do inet_pton() and inet_ntop() convert addresses? Why are they better than the old inet_aton()/inet_ntoa() pair?

  4. What does getaddrinfo() return, and why must you walk the list and then call freeaddrinfo()? Which old functions does it replace?

  5. What is the difference between Internet domain stream (TCP) and datagram (UDP) sockets? Where are service names looked up, and where are host names?

Khép lại chương Closing the chapter

Lập trình socket Internet là nghệ thuật khiến hai máy xa lạ hiểu nhau: quy mọi con số về network byte order, cất địa chỉ và cổng trong các cấu trúc chuẩn, và để con người gõ tên trong khi getaddrinfo() lặng lẽ dịch sang địa chỉ cho cả IPv4 lẫn IPv6. Nắm bộ công cụ ấy, bạn đã sẵn sàng viết những chương trình mạng khả chuyển thực thụ.1

Nói chuyện qua Internet gọn lại ở ba việc: xếp byte theo kiểu chung, bỏ địa chỉ và cổng vào hộp chuẩn, và để máy tra tên thành địa chỉ. Hiểu ba việc này là viết được chương trình mạng. Nhớ nhé: byte chung, hộp chuẩn, tra tên thành số.

Internet socket programming is the art of making two strangers understand each other: reduce every number to network byte order, store addresses and ports in standard structures, and let humans type names while getaddrinfo() quietly translates to addresses for both IPv4 and IPv6. Master that toolkit and you are ready to write genuinely portable network programs.1