Linux网络编程套接字
- 零、前言
- 一、网络基础知识
- 1、源IP地址和目的IP地址
- 2、源MAC地址和目的MAC地址
- 3、认识端口号
- 4、PORT VS PID
- 5、TCP和UDP协议
- 6、网络字节序
- 二、socket编程接口
- 1、sockaddr结构
- 2、socket 常见API
零、前言
本章就Linux网络编程进行概念及接口学习,下一篇则是简单的进行上手网络套接字编程
一、网络基础知识
1、源IP地址和目的IP地址
- 在数据传输时各网络协议栈会对数据进行报头封装,而在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
- 网络中每台计算机都有一个唯一的IP地址,也就是说网络中用IP可以标识唯一的一台计算机
- 数据传输需要知道目标主机,也就是目的IP;同样的目标主机在收到数据时也需要知道数据是哪一个主机发过来的,也就是源IP,在目标主机收到消息后也能通过源IP对发出数据主机作出响应
2、源MAC地址和目的MAC地址
- 大部分数据的传输都是跨局域网的,数据在传输过程中会经过若干个路由器,最终才能到达对端主机
- 源MAC地址和目的MAC地址是包含在链路层的报头当中的,而MAC地址实际只在当前局域网内有效,因此当数据跨网络到达另一个局域网时,其源MAC地址和目的MAC地址就需要发生变化
- 当数据达到路由器时,路由器会对数据进行分发和路由选择,根据源IP和目的IP进行决定下一个MAC地址,此时该数据的源MAC地址和目的MAC地址就发生了变化
- 因此数据在传输的过程中是有两套地址:一套是源IP地址和目的IP地址,这两个地址在数据传输过程中基本是不会发生变化的;另一套就是源MAC地址和目的MAC地址,这两个地址是一直在发生变化的
3、认识端口号
- 在实际的传输中,并不是数据在主机中的传输,而是需要将数据传输给对应的进程,所以在数据传输的过程中我们除了需要源IP和目的IP,还需要端口号
- 从本质上来说,数据的网络传输其实是进程间通信,只不过此时进程间的临界资源变成了网络
- 端口号(port)是传输层协议的内容,端口号是一个2字节16位的整数
- 端口号用来标识主机中的一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
- IP地址 端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用,保证标识进程的唯一性,存在一个进程占有多个端口号,但是一个端口号不能被多个进程占有
4、PORT VS PID
- 进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念
- 端口号(port)是网络数据传输中标识主机中进程的唯一性的,它是属于网络的概念
- 主机中并不是所有的进程都要进行网络通信,大部分的进程是不需要进行网络通信的本地进程,此时PID虽然也可以标识这些网络进程的唯一性,但在该场景下就不太合适了
- 在不同的场景下可能需要不同的编号来标识某种事物的唯一性,因为这些编号更适合用于该场景
5、TCP和UDP协议
传输层最典型的两种协议就是TCP协议和UDP协议
- TCP协议
- TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
- TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法
- UDP协议
- UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议
- 使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的
- 怎么理解协议的可靠与不可靠的
- 可靠的背后是需要付出代价的,TCP为了保证数据传输的可靠性需要更加复杂的实现,对应的其数据传输的效率必然会相比于UDP有减低
- UDP协议不可靠,但UDP协议在底层不需要做过多的工作,且它能够快速的将数据发送给对方,但是风险是数据传输没有保障
- 编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景:如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单
6、网络字节序
- 我们知道,不同的主机在存储数据时是有大小端之分的,同样的网络数据流也有大端小端之分
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- 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位的长整数从主机字节序转换为网络字节序
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
二、socket编程接口
1、sockaddr结构
- 套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)
- 因此套接字提供了
sockaddr_in
结构体和sockaddr_un
结构体,其中sockaddr_in
结构体是用于跨网络通信的,而sockaddr_un
结构体是用于本地通信的 - 为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了
sockeaddr
结构体,该结构体与sockaddr_in
和sockaddr_un
的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族 - 函数接口会根据结构体头部16个字节填入的协议家族类型进行判断真正的结构体类型
- 示图:
- 解释:
- 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_in 结构和in_addr结构:
- 注意:
- 虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in
- sockaddr_in结构里主要有三部分信息:地址类型,端口号,IP地址
- in_addr用来表示一个IPv4的IP地址,其实就是一个32位的整数
2、socket 常见API
- 函数原型:
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 服务器)
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);