网络编程--Socket(套接字)
Posted A-祥子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程--Socket(套接字)相关的知识,希望对你有一定的参考价值。
网络编程
网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。网络编程中
有两个主要的问题,一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后
如何可靠高效的进行数据传输。在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的
路由,由IP地址可以唯一地确定Internet上的一台主机。而TCP层则提供面向应用的可靠的
或非可靠的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据
的。
目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务
器等待客户提出请求并予以响应。客户则在需要服务时向服务器提出申请。服务器一般作为
守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客
户,同时自己继续监听服务端口,使后来的客户也能及时得到服务。
在Internet上IP地址和主机名是一一对应的,通过域名解析可以由主机名得到机器的IP,
由于机器名更接近自然语言,容易记忆,所以使用比IP地址广泛,但是对机器而言只有IP地
址才是有效的标识符。
通常一台主机上总是有很多个进程需要网络资源进行网络通讯。网络通讯的对象准确的讲
不是主机,而应该是主机中运行的进程。这时候光有主机名或IP地址来标识这么多个进程显然
是不够的。端口号就是为了在一台主机上提供更多的网络资源而采取得一种手段,也是TCP层
提供的一种机制。只有通过主机名或IP地址和端口号的组合才能唯一的确定网络通讯中的对象:
进程。
套接字
所谓socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。应用程
序通常通过"套接字"向网络发出请求或者应答网络请求。
套接字可以根据通信性质分类,这种性质对于用户是可见的。应用程序一般仅在同一类的
套接字间进行通信。不过只要底层的通信协议允许,不同类型的套接字间也照样可以通信。套
接字有两种不同的类型:流套接字和数据报套接字。
下面的解释比较抽象,不看也罢。
套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作
不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接
字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套
接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行
某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
套接字工作原理
要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为
ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个
步骤:服务器监听,客户端请求,连接确认。
所谓服务器监听,是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的
状态,实时监控网络状态。
所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接
字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的
地址和端口号,然后就向服务器端套接字提出连接请求。
所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它
就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦
客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他
客户端套接字的连接请求。
套接字地址结构
struct in_addr { in_addr_t s_addr; // 32-bit IPv4 address //network byte ordered } struct sockaddr_in { sa_family_t sin_family; //AF_INET in_port_t sin_port; //16-bit TCP or UDP port nummber, network byte ordered struct in_addr sin_addr; //32-bit IPv4 address, network byte ordered char sin_zero[8]; //unused }
sockaddr_in是网络套接字地址结构,大小为16字节,定义在<netinet/in>头文件中,一般我们在程序中是使用该结构体,但是作为参数传递给套接字函数时需要强转为sockaddr类型,注意该结构体中port和addr成员是网络序的(大端结构)。
struct sockaddr { sa_family_t sa_family; //address family: AF_XXX value char sa_data[14]; //protocol-specific address }
sockaddr是通过套接字地址结构,当作为参数传递给套接字函数时,套接字地址结构总是以指针方式来使用,比如bind/accept/connect函数等。
htons、ntohs、htonl和ntohl函数
#include <netinet/in.h> uint16_t htons(uint16_t host16bitvalue); uint32_t htonl(uint32_t host32bitvalue); uint16_t ntohs(uint16_t net16bitvalue); uint32_t ntohl(uint32_t net32bitvalue);
Linux提供了4个函数来完成主机字节序和网络字节序之间的转换。这些函数名字中,h表示host,n表示net,s表示short,l表示long。使用这些函数时,并不关心主机字节序和网络字节序的真实值,也就是为大端还是小端,要做的只是调用适当的函数在主机和网络字节序之间转换为某个特定值。
inet_aton、inet_addr和inet_ntoa函数
#include <arpa/inet.h> int inet_aton(const char *strptr, struct in_addr *addrptr); // 返回:若字符有效则为1,否则为0 in_addr_t inet_addr(const char *strptr); // 返回:若字符串有效则为32位二进制网络字节序地址,否则为INADDR_NONE char *inet_ntoa(struct in_addr inaddr); // 返回:指向一个点分十进制数串的地址
inet_aton、inet_addr和inet_ntoa在点分十进制数串(比如"192.168.1.1")与它长度为32位的网络字节序二进制值间转换IPv4地址。在调用inet_addr时需特别注意,inet_ntoa函数的输入参数是unsigned int型的ip地址,返回的却是指向ip字符串的指针,很明显,ip字符串所占的内存是在函数内部分配的,而我们并不需要释放该内存,所以,它分配的内存是静态的,内部使用static变量存储IP点分十进制数串,也就是说第二次调用该函数时会覆盖第一次调用该函数时的内存。
inet_pton和inet_ntop函数
#include <arpa/inet.h> int inet_pton(int family, const char *strptr, void *addrptr); // 返回:成功为1,输入不是有效表达式返回0,出错为-1 const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); // 返回:成功为指向结果的指针,出错为NULL
这两个函数对于IPv4和IPv6都适用,p代表表达式(presentation)、n表示数值(numeric)。第一个函数尝试转化由strptr指针所指的字符串,通过addptr指针存放二进制结果,成功返回1,如果对指定的family而言输入的不是有效的表达格式,那么返回0
inet_ntop进行相反的操作,如果len的值太小,不足以存放表达式结果,则返回一个空指针,并置error为ENOSPC。inet_ntop函数的strptr参数不可以是一个空指针,调用者必须为目标存储单元分配内存并制定其大小,调用成功时,这个指针就是该函数返回值。
socket函数
为了执行网络IO,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型(比如使用IPv4的TCP、使用IPv6的UDP、Unix域字节流协议)和套接字字类型(字节流、数据报或原始套接字)。
#include <sys/socket.h> int socket(int family, int type, int protocol); // 成功返回非负描述符,出错-1
family指定协议族,type指定套接字类型,protocol指定某个协议类型常值,或者设为0。
family的值有:
- AF_INET IPv4协议
- AF_INET6 Ipv6协议
- AF_LOCAL Unix协议域
- AF_ROUTE 路由套接字
- AF_KEY 秘钥套接字
type的值有:
- SOCK_STREAM 字节流套接字
- SOCK_DGRAM 数据报套接字
- SOCK_SEQPACKET 有序分组套接字
- SOCK_RAW 原始套接字
protocol的值有:
- IPPROTO_CP TCP传输协议
- IPPROTO_UDP UDP传输协议
- IPPROTO_SCTP SCTP传输协议
socket函数在成功时返回一个小的非负整数值,与文件描述符类似,成为套接字描述符,为了得到这个描述符,需要指定协议族和套接字类型,但是并没有指定本地协议地址和远端协议地址。
connect函数
#include <sys/socket.h> int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen); // 返回:成功为0,出错-1
TCP客户用connect函数来建立一个与TCP服务器连接,sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是指向一个套接字地址结构的指针和该结构的大小,套接字结构必须含有服务器的IP地址和端口号。注意:如果connect失败后,就必须close当前的套接字描述符并重新调用socket。客户端在调用connect前不必非得调用bind函数(比如UDP客户端编程中一般就不用调用bind),内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或出错时才返回。注意:connect是在接收到服务端响应的SYN+ACK时的返回的,也就是三次握手的第二次动作之后。
UDP是可以调用connect函数的,但是UDP的connect函数和TCP的connect函数调用确是大相径庭的,这里没有三次握手过程。内核只是检查是否存在立即可知的错误(比如目的地址不可达),记录对端的IP和端口号,然后立即返回调用进程。使用了connect的UDP编程就可不必使用sendto函数了,直接使用write/read即可。
bind函数
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); // 返回:成功为0,出错-1
bind函数把一个本地协议地址赋予一个套接字,它只是把一个协议地址赋予一个套接字,至于协议地址的含义则取决于协议本身。第二个参数指向协议地址结构的指针,第三个参数是协议地址的长度,对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,或两者都指定,也可以两者都不指定。
bind函数绑定特定的IP地址必须属于其所在主机的网络接口之一,服务器在启动时绑定它们众所周知的端口,如果一个TCP客户端或服务端未曾调用bind绑定一个端口,当调用connect或listen时,内核就要为响应的套接字选择一个临时端口。让内核选择临时端口对于TCP客户端来说是正常的额,然后对于TCP服务端来说确实罕见的,因为服务端通过他们众所周知的端口被大家认识的。
listen函数
#include <sys/socket.h> int listen(int sockfd, int backlog); // 返回:成功返回0,出错-1
socket创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的一个客户套接字。listen函数把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求,调用listen函数将导致套接字从CLOSEE状态转换到LISTEN状态。第二个参数规定了内核应为相应套接字排队的最大连接个数。
- 未完成连接队列:每一个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
- 已完成连接队列:每个完成TCP三路握手过程的客户对应其中一项,这些套接字处于ESTABLISHED状态。
图片来自《UNIX网络编程-卷一》
backlog参数在不同的系统中有不同的解释,不过大致类似。UNP(第3版)给出的定义为:listen()的backlog应该指定某个给定套接字上内核为之排队的最大已完成连接数。
当一个客户端SYN达到时,若这些队列是满的,TCP就忽略该分节,也即是不发送RST,这样做是暂时的,客户端将重新发送SYN,期望不就就能得到服务。假如服务端响应一个RST,客户端的connect就会返回错误,而不是让重传机制来处理,这样客户无法区分SYN的RST是因为"该端口没有在监听"还是"该端口在监听,只不过它的队列满了"。
在三路握手完成之后,但在服务端调用accept之前到达的数据应由服务端TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小。
在TCP服务端套接字编程中,执行完listen后,而没有执行accept,客户端是可以成功建立连接的,只不过是该连接被加入到了已连接队列中,当调用accept时会被提取出来。
accept函数
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); // 返回:成功返回已连接描述符(非负),出错-1
accept函数有TCP服务器调用,用于从已完成队列中列头返回下一个已完成连接,如果已完成队列为空,则进程被投入睡眠(如果该套接字为阻塞方式的话)。如果accept成功,那么其返回值是由内核自动生成的一个全新套接字,代表与返回客户的TCP连接,函数的第一个参数为监听套接字,返回值为已连接套接字。
close函数
#include <unistd.h> int close(int sockfd); // 若成功返回0,出错-1
close一个TCP套接字的默认行为是把该套接字标记为已关闭,然后立即返回到调用进程。注意,close实质把该套接字引用值减1,如果该引用值大于0,则对应的套接字不会被真正关掉。
服务器、客户端交互流程图
TCP状态转换图
getsockname和getpeername函数
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *localaddr, &addrlen); int getpeername(int sockfd, struct sockaddr *peeraddr, &addrlen); // 返回:成功为0, 出错为-1
getsockname获取sockfd对应的本端socket地址,并将其存储于address参数指定的内存地址,该socket长度存储于addrlen指向的变量中。getpeername获取远端的socket地址。
UDP客户端如果调用connect之后也是可以使用getpeername的。
recv和send函数
#include <sys/socket.h> ssize recv(int sockfd, void *buff, size_t nbytes, int flags); ssize send(int sockfd, void *buff, size_t nbytes, int flags); // 返回:成功为读入或写入的字节数,出错为-1
TCP流数据读写操作函数。flag取值如下所示:
- MSG_OOB 对于send,表明将要发送带外数据,TCP连接上只有一个字节可以作为带外数据发送,对于recv,本标志表明即将要读入的是带外数据而不是普通数据。
- MSG_PEEK 该标志适用于recv和recvfrom,它允许我们查看已可读取的数据,而且在系统不在recv和recvfrom返回丢弃其这些数据
注意的是,flags参数只对send和recv的当前调用有效,当然也可以通过setsockopt系统调用永久性地修 改socket的某些属性。
以上是关于网络编程--Socket(套接字)的主要内容,如果未能解决你的问题,请参考以下文章