计算机网络——网络编程
网络编程核心是实现跨主机的进程通信。同一台电脑用管道、共享内存等通信。不同电脑就得靠 → IP 端口 Socket TCP/UDP。一主机字节序列和网络字节序列主机字节序列分为大端字节序和小端字节序不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处而低位字节则存储在内存的低地址处。在两台使用不同字节序的主机之间传递数据时可能会出现冲突。所以在将数据发送到网络时规定整形数据使用大端字节序所以也把大端字节序成为网络字节序列。对方接收到数据后可以根据自己的字节序进行转换。大端:手机,网络小端:电脑二字节序转换函数Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换头文件#include netinet/in.h1. htonl() —— 主机 → 网络32 位长整型uint32_t htonl(uint32_t hostlong); //长整型的主机字节序转网络字节序功能将 32 位的主机字节序整数转换为网络字节序大端典型用途转换 IP 地址IPv4 地址是 32 位整数struct sockaddr_in ser_addr; ser_addr.sin_addr.s_addr inet_addr(127.0.0.1); // 内部已做 htonl // 或等价写法ser_addr.sin_addr.s_addr htonl(INADDR_ANY);2.ntohl() —— 网络 → 主机32 位长整型uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序功能将 32 位的网络字节序大端整数转换回主机字节序典型用途解析收到的 IP 地址struct sockaddr_in cli_addr; inet_ntoa(cli_addr.sin_addr); // 内部已做 ntohl3.htons() —— 主机 → 网络16 位短整型uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序功能将 16 位的主机字节序整数转换为网络字节序大端典型用途转换 端口号端口号是 16 位整数struct sockaddr_in ser_addr; ser_addr.sin_port htons(5000); // 必须转4.ntohs() —— 网络 → 主机16 位短整型uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序功能将 16 位的网络字节序大端整数转换回主机字节序典型用途解析收到的 端口号struct sockaddr_in cli_addr; printf(port: %d\n, ntohs(cli_addr.sin_port)); // 必须转三套接字地址结构我们前面所讨论的IPC机制都依靠一台计算机系统的共享资源实现这里的资源可以是文件系统空间共享的物理内存或者消息队列但只有运行在同一台机器上的进程才能使用它们。一台机器上的进程可以使用套接字和另外一台机器上的进程通信,这样就可以支持分布在网络中的客户/服务器系统。同一台机器上的进程之间也可以使用套接字进行通信。套接字地址结构是操作系统用来描述 “网络通信端点“的结构体它把 IP 地址 端口号 协议类型 打包在一起让系统知道数据要发给谁、从哪里收。套接字的特性由3个属性确定,它们是域(domain)类型(type)和协议(protocol)。套接字用地址作为它的名字地址的格式随”域“(又被称为协议族,protocol family)的不同而不同每个协议族又可以使用一个或多个地址族来定义地址格式。1.域Domain 协议族Protocol Family作用规定了套接字的地址格式和通信范围。比如AF_INETIPv4 协议族地址是 IP地址 端口号用于跨主机网络通信。AF_UNIX本地协议族地址是文件路径只能在同一台机器内的进程间通信。2.类型Type 服务类型1SOCK_STREAM流式套接字对应 TCP 协议可靠、面向连接、有序、无差错、不丢失。像打电话先拨号建立连接通话时数据稳定有序挂断后断开。2SOCK_DGRAM数据报套接字对应 UDP 协议不可靠、无连接、只管发不管到。像寄信直接把信丢出去不保证对方一定收到也不保证顺序。3.协议Protocol通常填 0系统会根据域和类型自动选择最合适的协议AF_INET SOCK_STREAM → 自动选 TCPAF_INET SOCK_DGRAM → 自动选 UDP只有在极少数需要明确指定协议时才写具体值如 IPPROTO_TCP。3.1通用socket地址结构socket 网络编程接口中表示 socket 地址的是结构体 sockaddr其定义如下#include bits/socket.h struct sockaddr { sa_family_t sa_family; // 协议族/地址族比如 AF_INET 表示 IPv4 char sa_data[14]; // 地址数据缓冲区占位用具体内容由 sa_family 决定 };1. sa_family类型sa_family_t本质是无符号短整型作用告诉系统这个地址属于哪个协议族决定了 sa_data 里的数据该怎么解析。常见取值AF_INETIPv4 协议族AF_INET6IPv6 协议族AF_UNIX本地 Unix 域协议族用文件路径做地址2. sa_data[14]作用占位缓冲区用来存放具体的地址数据IP 端口、文件路径等。它本身没有任何格式只是一块 14 字节的内存具体含义完全由 sa_family 决定如果是 AF_INETsa_data 里存的是 struct sockaddr_in 的 IP 端口 填充字节如果是 AF_UNIXsa_data 里存的是一个文件路径字符串你永远不会直接读写 sa_data而是通过具体的地址结构如 sockaddr_in来操作。3.2专用 socket 地址结构专用 Socket 地址结构 是为了方便程序员操作特定协议而设计的结构体。因为通用的 struct sockaddr 只有一个占位数组 sa_data[14]你无法直接操作 IP 和端口所以系统提供了专用结构来替代 sa_data让我们用字段名直接读写 IP 和端口。在网络编程中最常用的专用结构是 struct sockaddr_inIPv4 专用。TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体它们分别用于 IPV4 和 IPV6IPV4#include netinet/in.h struct sockaddr_in { sa_family_t sin_family; // 地址族 (AF_INET) → 对应 sa_family in_port_t sin_port; // 16位端口号 → 存在 sa_data[0-1] struct in_addr sin_addr; // 32位IP地址 → 存在 sa_data[2-5] char sin_zero[8]; // 填充字节 (必须置0) → 对应 sa_data[6-13] }; // 内部IP结构 struct in_addr { uint32_t s_addr; // 32位无符号整数 (网络字节序) };IPV6struct in6_addr { unsigned char sa_addr[16]; // IPV6 地址要用网络字节序表示 }; struct sockaddr_in6 { sa_family_t sin6_family; // 地址族 AF_INET6 u_int16_t sin6_port; // 端口号用网络字节序表示 u_int32_t sin6_flowinfo; // 流信息应设置为 0 struct in6_addr sin6_addr; // IPV6 地址结构体 u_int32_t sin6_scope_id; // scope ID尚处于试验阶段 };3.3IP 地址转换函数通常人们习惯用点分十进制字符串表示 IPV4 地址但编程中我们需要先把它们转化为整数方能使用下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换。作用把人类能看懂的字符串 IP ↔ 计算机能看懂的网络字节序整数人类看得懂192.168.1.100字符串计算机看得懂0xC0A80164网络字节序整数IPv4 最常用两个转换函数1.inet_addr() 字符串 IP → 网络字节序整数#include arpa/inet.h in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网络字节序例子把 127.0.0.1 → 转成网络字节序的 32 位整数。struct sockaddr_in ser_addr; ser_addr.sin_addr.s_addr inet_addr(127.0.0.1);2.inet_ntoa() 网络字节序整数 → 字符串 IP#include arpa/inet.h inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示例子把网络序整数 → 转成 192.168.x.x 格式字符串。printf(IP: %s\n, inet_ntoa(cli_addr.sin_addr));3.4网络编程接口网络编程接口 操作系统给我们提供的一套函数用来写网络通信代码。也就是我们常说的 Socket 接口套接字接口。TCP 网络编程 核心接口1.socket () —— 创建套接字作用创建一个网络套接字返回文件描述符 fdint socket(int domain, int type, int protocol); //socket()创建套接字成功返回套接字的文件描述符失败返回-1 //domain: 设置套接字的协议簇 AF_UNIX AF_INET AF_INET6 //type: 设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM // protocol: 一般设置为 0表示使用默认协议参数详解1.domain协议族 / 地址族AF_INETIPv4AF_INET6IPv62.type套接字类型SOCK_STREAMTCP 流式SOCK_DGRAMUDP 数据报3.protocol协议编号一般写 0自动匹配2.bind () —— 绑定 IP 端口作用给服务端设置 “地址门牌”int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //bind()将 sockfd 与一个 socket 地址绑定成功返回 0失败返回-1 //sockfd 是网络套接字描述符,(命名套接字,就是上面的函数的返回值作为了我们的参数sockfd) //addr 是地址结构 //addrlen 是 socket 地址的长度参数1,sockfdsocket () 返回的文件描述符2.addr本机地址结构体指针IPv4struct sockaddr_in强转成 (struct sockaddr*)3.addrlen地址长度 sizeof(结构体)3.listen () —— 监听作用让套接字进入监听状态等待客户端连接int listen(int sockfd, int backlog); //listen()创建一个监听队列以存储待处理的客户连接成功返回 0失败返回-1 //sockfd 是被监听的 socket 套接字 //backlog 表示处于完全连接状态的 socket 的上限参数1.sockfd监听套接字2.backlog等待连接队列的最大长度一般写 5/104.accept () —— 接受连接作用阻塞等待客户端连接返回新 fd 用于通信int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //accept()从 listen 监听队列中接收一个连接成功返回一个新的连接 socket //该 socket 唯一地标识了被接收的这个连接失败返回-1 //sockfd 是执行过 listen 系统调用的监听 socket //addr 参数用来获取被接受连接的远端 socket 地址 //addrlen 指定该 socket 地址的长度参数sockfd监听 fdaddr输出型参数 → 保存客户端的 IP 端口addrlen客户端地址长度必须传地址5.connect () —— 客户端连接服务端作用客户端主动连接服务器int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); //connect()客户端需要通过此系统调用来主动与服务器建立连接 //成功返回 0失败返回-1 //sockfd 参数是由 socket()返回的一个 socket。 //serv_addr 是服务器监听的 socket 地址 //addrlen 则指定这个地址的长度参数sockfd客户端套接字serv_addr服务端地址IP 端口addrlen地址长度6.send () —— 发送数据ssize_t send(int fd, const void *buf, size_t len, int flags); //send()往 socket 上写入数据 buff 和 len 参数分别指定写缓冲区的位置和数据长度参数fd通信套接字buf要发送的数据缓冲区len数据长度flags标志位一般写 07.recv () —— 接收数据ssize_t recv(int fd, void *buf, size_t len, int flags); //recv()读取 sockfd 上的数据 buff 和 len 参数分别指定读缓冲区的位置和大小参数fd通信套接字buf接收缓冲区len最大接收长度flags一般写 08.close () —— 关闭close(int fd); //close()关闭一个连接实际上就是关闭该连接对应的 socketUDP 协议 专用的数据读写接口1. sendto() —— UDP 发送数据ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);作用像寄快递必须写清楚 “收件人地址IP 端口”。参数详解sockfd套接字文件描述符。buff要发的数据内容。len数据长度。flags标志位一般填 0。dest_addr重点目标地址告诉系统数据要发给谁填对方的 IP 端口addrlen地址结构的大小sizeof(struct sockaddr_in)2. recvfrom() —— UDP 接收数据ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen);作用像收快递必须知道 “谁寄给我的”才能回复.参数详解sockfd套接字文件描述符。buff接收数据的缓冲区。len缓冲区大小。flags标志位一般填 0。src_addr重点源地址输出参数函数会把发送方的 IP 端口填到这里面让你知道是谁发的。addrlen地址结构的大小传入指针。核心区别为什么 UDP 要用这俩特性TCP (send/recv)UDP (sendto/recvfrom)连接状态已连接三次握手建好直接发无连接没建连接必须带地址发送方向直接写给fd必须带dest_addr接收方向直接从fd读必须带src_addr记录谁发的简单比喻TCP 像打电话接通后直接说话不需要每次都说 “我是张三”。UDP 像寄信每次写信都必须在信封上写 “北京市 xx 路 xx 号”否则寄不出去。UDP 也可以用 send/recv可以但有条件。如果你用 connect() 给 UDP socket 设置了固定目标就可以用 send() 发不用每次带地址。就可以用 recv() 收只收那个目标的。但最标准、最通用的 UDP 写法还是用 sendto / recvfrom因为它支持多播 / 广播可以动态改变发送目标。总结sendtoUDP 发数据必须带目标地址。recvfromUDP 收数据必须带源地址记录是谁发的。这就是 UDP 无连接特性的体现