socket套接字
端口号
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
理解 “端口号” 和 “进程ID”
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;后面再详细讨论
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
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表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
socket编程接口
netstat
是一个用于显示网络连接、路由表和网络接口等网络相关信息的命令行工具。它通常被用于诊断网络问题、监控网络连接等。
netstat
命令的使用方法可以因操作系统而异,但通常包括以下常见选项:
-a
:显示所有连接和侦听端口。-n
:以数字格式显示地址和端口。-p
:显示与连接相关的进程。-r
:显示路由表。-i
:显示网络接口信息。-u
:UDP-t
:TCP
socket 常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
// domain:表示地址族,指定套接字的通信域。常见的取值包括 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(UNIX Domain Socket)等
// type:表示套接字类型,指定套接字的通信方式 SOCK_STREAM(TCP),
// protocol:表示协议类型,通常设置为 0,表示使用默认协议在某些情况下,需要指定具体的协议,比如对于 IPv4 地址族,可以选择使用 IPPROTO_TCP(TCP 协议)或 IPPROTO_UDP(UDP 协议)等。如果传入 0,则根据 domain 和 type 的值自动选择合适的默认协议。
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。
struct sockaddr_in:
struct sockaddr_in
是用于表示 IPv4 地址信息的结构体。- 该结构体通常用于在网络编程中表示 Internet 地址,包括 IP 地址和端口号。
- 成员包括
sin_family
(地址族,通常为AF_INET
)、sin_port
(端口号)和sin_addr
(IP 地址)等。 - 在网络编程中,常用的函数如
socket()
、bind()
、connect()
、accept()
等都会使用struct sockaddr_in
结构体来表示地址信息。
struct sockaddr_un:
struct sockaddr_un
是用于表示 UNIX 域套接字地址信息的结构体。- 该结构体通常用于在本地进程之间进行通信,不涉及网络。
- 成员包括
sun_family
(地址族,通常为AF_UNIX
)和sun_path
(套接字路径)等。 - UNIX 域套接字允许本地进程之间通过文件系统路径进行通信,比如在同一台主机上的不同进程之间进行通信。
常见的地址族包括:
- AF_INET(IPv4):用于表示 IPv4 地址,包括 32 位的 IP 地址和 16 位的端口号。
- AF_INET6(IPv6):用于表示 IPv6 地址,包括 128 位的 IP 地址和 16 位的端口号。
- AF_UNIX(UNIX Domain Socket):用于本地进程之间的通信,通常使用文件系统路径来表示套接字地址。
- AF_PACKET:用于直接访问网络接口,可以发送和接收数据帧。
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数
sockaddr 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
in_add 结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址。
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换。
字符串转in_addr的函数:
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
in_addr转字符串的函数:
char* inet_ntoa(struct in_addr inaddr);
TCP socket API 详解
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。
-
socket();
int socket(int domain, int type, int protocol);
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol参数的介绍从略,指定为0即可
-
bind();
// 绑定端口号 (TCP/UDP, 服务器) int bind(int socket, const struct sockaddr *address, socklen_t address_len);
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
- 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
-
listen();
// 开始监听socket (TCP, 服务器) int listen(int socket, int backlog);
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究;
- listen()成功返回0,失败返回-1;
-
accept();
// 接收请求 (TCP, 服务器) int accept(int socket, struct sockaddr* address, socklen_t* address_len);
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr 参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
-
connect();
// 建立连接 (TCP, 客户端) int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
- connect()成功返回0,出错返回-1
关于TCP UDP的通信方式,我们会在后文详解。