本章是承前启后的一章,他探讨linux网络编程API与内核中TCP/IP协议族之间的关系,并为后续章节提供编程基础。我们将讨论linux网络API:
1、网络字节序和主机字节序
现代CPU累加器一次都能装载至少4个字节(32位机器),即一个整数。那么这4个字节在内存中的排列顺序将影响他被累加器装载成的整数的值。这就是字节序问题。字节序(大小端)大端:高位字节存放在内存的低地址处地位。小端:高位字节存放在内存的高地址处。
现代PC大多数采用小端字节序,因此小端字节序又被称为主机字节序。
当格式化的数据(32位整型和16位短整型)在两台不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决的办法是:发送端总是把要发送的数据转换成大端字节序然后在发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也被称为网络字节序,他给所有接收数据的主机提供了一个正确解释受到的格式化数据的保证。
需要指出,即使是同一台机器上的两个进程(一个由VC编写,一个由JAVA编写通信)也要考虑字节序的问题(JAVA虚拟机采用大端字节序)
linux提供了四个函数来完成主机字节序到网络字节序的转换:
#include<netinet/in.h>
(1)unsigned long int htonl(unsigned long int hostlong);
(2)unsigned short int htons(unsigned short int hostshort);
(3)unsigned long int ntohl(unsigned long int netlong);
(4)unsigned short int htons(unsigned short int netshort);
他们的含义很明确,比如htonl表示“host to network long”即将长整型(32位)的主机字节序(小端字节序)转换成网络字节序(大端字节序)。
长整型函数通常用来转换IP地址,短整型函数用来转换端口号(淡然不限制于此。任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序)
2、通用socket地址:
socket网络编程接口中表示socket地址的是sockaddr,其定义如下:
struct sockaddr
{
sa_family sa_family;
char sa_sa_data[14];
}
sa_family是地址族类型变量。地址族类型通常与协议族类型对应。协议族PF_*,地址族AF_*。
sa_data成员用于存放socket地址值。不同协议族的地址值具有不同的含义和长度。
3、专用socket地址:
struct sockaddr_in
{
sa_family_t sin_family;//地址族
u_int16_t sin_port;//端口号,永网络字节序表示
struct in_addr sin_addr;//ipv4结构体
};
struct in_addr
{
u_int32_t s_addr;//ipv4地址,要用网络字节序表示。(大端字节序)
}
4、IP地址转换函数:
人们通常使用点分十进制表示ipv4地址,用点分16进制表示ipv6地址。但编程中,我们要首先把它们转换成整数(2进制数)方能使用。下面3个函数用于点分十进制表示的ipv4地址和用网路字节序表示的ipv4进行转换:
#include<arpa/inet.h>
in_addr_t inet_addr(const char * strptr);
int inet_aton(const char * cp,struct in_addr *inp);
char * inet_ntoa(struct in_addr in);
inet_addr函数将用点分十进制字符转表示的ipv4地址转换称为用网络字节需表示的
ipv4地址。它失败时返回 INADDR_NONE。
inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储与参数inp指向的地址结构中。成功时返回1,失败返回0。
inet_ntoa函数将用于网络字节序整数表示的ipv4地址转化为用点分十进制字符串表示的ipv4地址。但要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。
5、创建socket:
linux的一个哲学就是,所有东西都是文件。socket也不列外,他就是可读可写可控制,可关闭的文件描述符。
#include<sys/type.h>
#include<sy/socket.h>
int socket(int domain , int type , int protocol );
(1)domain告诉系统调用哪个底层协议族。
(2)type参数指定服务类型。
(3)protocol这个值几乎在所有情况下都为0.
6、命名socket:
创建socket时,我们给他制定了地址族,但是并未制定使用该地址族中的哪个具体的socket地址。讲一个socket与socket地址的绑定称为给socket命名。
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr * my_addr,socklen_t addrlen);
bind将my_addr所指的socket地址分配给为命名的sockfd文件描述符,addrlen参数指出socket地址的长度。
bind成功时返回0,失败则返回-1并设置errno。
7、监听socket:
socket命名之后还不能马上接受客户端连接,我们需要使用系统调用来创建一个监听队列以存放待处理的客户连接。
#include<syssocket.h>
int listen(int sockfd,int backlog);
sockfd参数指定被监听的socket。backlog参数指定最大连接数。
8、接受连接:
#include<sys/types.h>
#include<sys/socket.h>
int accept(itn sockfd , struct sockaddr* ,socklen_t * addrlen);
sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被接受连接的远端socket地址该socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接的客户端通信。accept失败时返回-1并设置errno。
9、发起连接:
如果说服务器通过listen调用来被动的接受连接,那么客户端需要通过如下系统注定的与服务器建立连接。
#include<sys/types.h>
#include<sys/socket.h>
int connetc ( int sockfd , const struct sockaddr * serv_addr,socklen_t addrlen);
sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务器监听地址,addrlen参数则指定了这个地址的长度。
connect成功时返回0.一旦成功建立连接,sockfd就唯一的标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置erron。
10、关闭连接:
close(sockfd);
二、数据读写:
1、 对文件的读写操作。socket编程接口提供了几个专门用于socket数据读写的系统调用,他们增加了对数据读写的控制。其中TCP流数据读写的系统调用是:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd, void * buf ,size_t len ,int flages);
ssize_t send(int sockfd, const void * buf, ssize_t len, int flages);
recv 读取sockfd上写入数据,buf 和 len参数 分别指定读缓冲区的位置和大小,flages参数通常设为0就好。
recv成功时返回实际读取到的数据的长度,他可能小于我们期望的的长度len。因此我们需要多次调用recv,才能读取到完整的数据。recv可能返回0。这意味着我们通信双发已经关闭连接了。recv出错时返回-1,并设置errno。
send往sockfd上写入数据据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败返回-1并设置errno。
2、地址信息函数:
在某些情况下,我们想知道一个连接socket的本段socket地址, 以及远端socket地址。
#include<sys/socket.h>
int getsockname(int sockfd ,struct sockaddr * address,socklen_t address_len);
int geupeername(int sockdf ,struct sockaddr * address,socklen_t * address_len);
getsocketname()获得本端socket地址,并将其存储在address参数指定的内存中,该socket地址的长度则存储于address_len 参数指向的变量中。
getpeername()获取sockfd对应的远端socket地址,其参数及返回值的含义语句getsockname参数及返回值相同。
三、socket函数详解:
1
2
3
4
5
6
|
# include <arpa/inet.h> int inet_pton( int family, const char * strptr, void * addrptr); //成功时返回1,格式不对返回0,出错返回-1. //作用:p代表表达式,n代表数值 以后所写的所有代码中都有可能会需要这个函数,所以这个函数比较重要。 //将char所指向的字符串,通过addrptr指针存放 //他的反函数:inet_ntop()作用相反。 //需要注意的是:当它发生错误的时候,errno的值会被置为eafnosupport关于errno值等下再说。 |
1
2
3
|
# include <sys/socket.h> omt connect( int sockfd, const struct sockaddr * servaddr,socklen_t addrlen); //用connect函数来建立与TCP服务器的连接 |
1
2
3
|
# include <sys/socket.h> int bind( int sockfd, const struct sockaddr * servaddr,socklen_t addrlen); //把本地协议地址赋予一个套接字。也就是将32位的ipv4或者128位的ipv6与16位的TCP或者UDP相结合。 |
1
2
|
# include <unistd.h> int close( int sockfd); //关闭socket,并设置TCP连接 |
1
2
3
4
5
|
#Include<sys/socket.h> int accept( int sockfd,struct sockaddr * cliaddr ,socklen_t* addrlen); //成功时返回描述符,失败返回-1 1 、如果第二三个参数为空,代表了,我们对客户的身份并不感兴趣,因此置为NULL。 2 、第一个参数为socket创建的监听套接字,返回的是以连接套接字,两个套接字是有区别的。比较大的区别。区别:我们所创建的监听套接字一般服务器只创建一个,并且一直存在。而内核会为每一个服务器进程的客户连接建立一个连接套接字,当服务器完成对某个给定客户的服务时,连接套接字就会被关闭。 |
关于连接三次握手1和TCP连接关闭事后的分组交换。
三次握手:
为了更好的理解connect,bind,close,三个函数,了解一下TCP连接的建立和终止是很有必要的。
1、服务器必须先打开,等待准备接受外来地连接。
2、客户端时通过connect发起主动打开。
3、主动打开后,客户TCP发送了一个SYN(同步)分节,她告诉服务器客户将在连接中只发送的数据的初始序列号,SYN分节不携带数据。他发送的ip数据报,只有一个ip首部,一个TCP首部以及TCP选项。
4、服务器必须确认(ACK)客户的syn,同时自己也发送一个syn分节,它含有服务器将在统一链接中发送的数据的初始序列号。服务器在单个分节中发送SYN对客户SYN的ACK确认+1。
5、客户端必须确认服务器的SYN分节:
上面的过程被称为TCP的三次握手
注意:SYN是TCP/IP建立连接时实用的握手信号。在客户机和服务器之间建立正常的TCP网络连接时,客户机首先发出一个SYN消息,服务器使用SYN+ACK应答表示接受到了这个消息,最后客户机可以在以ACK消息相应。这样,在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。
TCP连接终止:
(1)通过调用close,我们执行主动关闭,TCP发送一个FIN(finish,表示结束),表示数据发送完毕。
(2)对端接收到fin后,执行被动关闭。
(3)一段时间之后,接收到的文件结束符的应用进程,将调用close关闭她的套接字,于是套接字也发送了一个fin。
(4)确认这个fin ack+1
(5)我们称它为tcp四次挥手
5、ipv4,ipv6套接字的地质结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { uint8_t sin_len; //无符号8位整型 sa_family_t sin_famliy; /*AF_INET*/ in_port_t sin_port; struct in_addr sin_addr; /*32位 IPv4 地址*/ char sin_zero[ 8 ]; /*unuse*/ }; //头文件 #include <sys/types.h> //sa_family_t和socklen_t 头文件 #include <sys/socket.h> //in_addr_t in_port_t 头文件 #include <netinet/in.h> |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
struct in6_addr { uint8_t s6_addr[ 16 ]; }; #define SIN6_LEN struct sockaddr_in6 { uint8_t sin6_len; sa_family_t sin6_famliy; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; }; |