在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?TCP/IP协议规定,网络数据流应采用大端字节序,即低地址存高字节,高地址存低字节。 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
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位短整数。 //如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示16 位短整数。 例如:htonl 表示将 32 位的长整数从主机字节序转换为网络字节序。
sockaddr数据结构: sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下:
struct sockaddr { sa_family_t sin_family;//地址族 char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息 };sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
//sockaddr_in结构 struct sockaddr_in { sa_family_t sin_family;//地址族 in_port_t sin_port;//16位端口号 struct in_addr sin_addr;//32位IP地址 char sin_zeros[8];//不使用 }; //in_addr结构 struct in_addr { uint32_t s_addr; };基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr sin_addr 表示 32 位的 IP 地址。但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换。
字符串转 in_addr 的函数:
#include <arpa/inet.h> int inet_aton(const char *strptr, struct in_addr *addrptr); in_addr_t inet_addr(const char *strptr); int inet_pton(int family, const char *strptr, void *addrptr);in_addr 转字符串的函数:
char *inet_ntoa(struct in_addr inaddr); const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);TCP连接的socket模型流程图: 对应API:
socket函数(创建socket) #include <sys/socket.h> int socket(int domain, int type, int protocol); domain: AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址 AF_INET6 与上面类似,不过是来用IPv6的地址 AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用 type: SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。 SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。 SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。 SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议) SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序 protocol: 传0 表示使用默认协议。 返回值: 成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno bind函数 #include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *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。 listen函数 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); sockfd: socket文件描述符 backlog: 排队建立3次握手队列和刚刚建立3次握手队列的链接数和 查看系统默认backlog cat /proc/sys/net/ipv4/tcp_max_syn_backlog 典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。未完成连接队列。客户发送的SYN已到达服务端,但服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_RCVD状态。
已完成连接队列。每个已完成TCP三次握手的套接字对应其中的一项。这些套接字处于ESTABLISHED状态。
如果三次握手正常完成,该套接字从未完成连接队列移到已完成队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果说队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
当一个客户的SYN到达时,若队列是满的,TCP就忽略该SYN,而不会发送RST,因为客户在规定时间内会重发SYN。
在三次握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排除,最大数据量为相应已连接套接字的接收缓冲区大小。
accept函数
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); sockdf: socket文件描述符 addr: 传出参数,返回链接客户端地址信息,含IP地址和端口号 addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小 返回值: 成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno connect函数 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockdf: socket文件描述符 addr: 传入参数,指定服务器端地址信息,含IP地址和端口号 addrlen: 传入参数,传入sizeof(addr)大小 返回值: 成功返回0,失败返回-1,设置errnoTCP连接的步骤: 服务器: 1.创建套接字描述符(socket) 2.设置服务器的IP地址和端口号(需要转换为网络字节序的格式) 3.将套接字描述符绑定到服务器地址(bind) 4.将套接字描述符设置为监听套接字描述符(listen),等待来自客户端的连接请求,监听套接字维护未完成连接队列和已完成连接队列 5.从已完成连接队列中取得队首项,返回新的已连接套接字描述符(accept),如果已完成连接队列为空,则会阻塞 6.从已连接套接字描述符读取来自客户端的请求(read) 7.向已连接套接字描述符写入应答(write) 8.关闭已连接套接字描述符(close),回到第5步等待下一个客户端的连接请求 客户端: 1.创建套接字描述符(socket) 2.设置服务器的IP地址和端口号(需要转换为网络字节序的格式) 3.请求建立到服务器的TCP连接并阻塞,直到连接成功建立(connect) 4.向套接字描述符写入请求(write) 5.从套接字描述符读取来自服务器的应答(read) 6.关闭套接字描述符(close)
UDP连接的socket模型流程图: UDP连接的步骤: 服务器: 1.创建套接字描述符(socket) 2.设置服务器的IP地址和端口号(需要转换为网络字节序的格式) 3.将套接字描述符绑定到服务器地址(bind) 4.从套接字描述符读取来自客户端的请求并取得客户端的地址(recvfrom) 5.向套接字描述符写入应答并发送给客户端(sendto) 6.回到第4步等待读取下一个来自客户端的请求
客户端: 1.创建套接字描述符(socket) 2.设置服务器的IP地址和端口号(需要转换为网络字节序的格式) 3.向套接字描述符写入请求并发送给服务器(sendto) 4.从套接字描述符读取来自服务器的应答(recvfrom) 5.关闭套接字描述符(close)
