以下是我学习网络编程的一些笔记。
Socket API
任何现代应用程序,如要访问互联网,必须通过Socket API. Socket这个单词的意思是“插座”,曾经被译为“插口”,现在一般翻译为“套接字”。程序员通常会把socket
简写为sock
.
Socket API最初是在BSD Unix上出现的,后面Linux系统也实现了一套相同的API. Windows系统的实现则是Windows Sockets API (简称WSA).
Windows对Socket API的实现并不正统。要严肃地学习网络编程,最好的方式是使用Linux系统和C语言。这方面最好的书莫过于《UNIX网络编程 卷1》(简称UNPv1). 《深入理解计算机系统》(简称CSAPP)的最后两章也进行了简单的介绍。
字节序
了解计算机底层的人,都知道小端序(Little Endian, LE)和大端序(Big Endian, BE).
小端和大端的英文源自《格列佛游记》里,两个国家的人在吃鸡蛋时,一个国家的人选择先磕碎鸡蛋较大的一端,另一国家的人选择先磕碎较小的一端(而且他们还会因此打起来)。
字节序决定了整数的存储方式。对于32位整数0x12345678
,如果使用小端序存储,则为:
78 56 34 12
如果使用大端序存储,则为:
12 34 56 78
现代PC都使用小端序存储数据。由于历史原因(那时的机器使用大端序),网络传输必须使用大端序,因此需要进行转换。
Linux系统提供了以下函数来完成转换:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
这些函数的命名存在规律:
h
表示host,即主机n
表示net,即网络l
表示long,即32位整数 (通常是IPv4地址)s
表示short,即16位整数 (通常是端口号)
小端序→大端序,大端序→小端序转换执行的操作一样的,所以htonX()
函数和ntohX()
函数的内部实现相同,给两个函数只是为了语义上的清晰。
地址
sockaddr_in
结构表示套接字的地址。
struct sockaddr_in {
uint16_t sin_family; // = AF_INET
uint16_t sin_port; // 端口
struct in_addr sin_addr; // IP地址
unsigned char sin_zero[8]; // 补齐
};
struct in_addr {
uint32_t s_addr;
};
对于已有的套接字:
- 如果要获得它的本地地址,使用
getsockname()
- 如果要获得它的远端地址,使用
getpeername()
系统头文件中定义了两个常用地址:
INADDR_ANY
即0.0.0.0
表示 所有的网络接口INADDR_LOOPBACK
即127.0.0.1
表示 本地环回
要将字符串表示的IP地址转换为其整数形式,可使用inet_addr()
. 如果要完成相反的操作,可使用inet_ntoa()
. 这两个函数并不支持IPv6. 所以推荐使用更为强大的inet_pton()
和inet_ntop()
.
创建套接字
无论是服务器端还是客户端网络编程,第一步都是获得一个套接字文件描述符。
int socket(int domain, int type, int protocol);
domain
表示套接字的域,取AF_INET
即可type
表示套接字的类型,可以理解为协议:SOCK_STREAM
表示TCP协议(因为TCP是基于字节流的)SOCK_DGRAM
表示UDP协议(因为UDP是基于数据报(Datagram)的)SOCK_RAW
表示,不使用运输层协议,直接通过IP协议通信
protocol
填0即可,具体的协议会根据type
自动选择
如果套接字创建失败了,此函数会返回-1.
服务器端
对服务器端来说,创建好套接字后,紧接着就是把它绑定(bind)到指定的地址,并开始监听(listen),接受(accept)客户端的请求。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int listenfd, struct sockaddr *addr, int *addrlen)