OSI七层模型
字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
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 表示要转换的数据是 4 字节,s 表示要转换的数据是 2 字节。
-
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
sockaddr数据结构
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
1. socketaddr_in结构体:
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; struct in_addr { uint32_t s_addr; /* address in network byte order */ };
套接字api
1. socket模型创建流程图
2. 主要函数:
1. 函数socket:
int socket(int domain, int type, int protocol);
分析:
-
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
2. 函数bind
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
分析:
- sockfd:socket文件描述符
- addr:构造出IP地址加端口号
- addrlen:sizeof(addr)长度
- 返回值:功返回0,失败返回-1, 设置errno
【注意】:服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:
struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666);
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。
3. 函数 listen:
int listen(int sockfd, int backlog);
分析:
- sockfd:socket文件描述符
- backlog:排队建立3次握手队列和刚刚建立3次握手队列的链接数和
4. 函数accept:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
分析:
- sockdf:socket文件描述符
- addr:传出参数,返回链接客户端地址信息,含IP地址和端口号
- addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
- 返回值:成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
5. 函数connect:建立与TCP服务器的连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockdf:socket文件描述符
- addr:传入参数,指定服务器端地址信息,含IP地址和端口号
- addrlen:传入参数,传入sizeof(addr)大小
- 返回值:成功返回0,失败返回-1,设置errno
【注意】:客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。