在学习Linux系统编程的时候,进程间的通信方式包括——管道、消息队列、共享内存、信号量等方式。但是这些通信方式都村子一定的缺陷——都是在同一个机器上的进程间的通信。为了让不同机器上的进程之间相互通信,Linux网络编程便可解决。linux系统支持套接字接口,可以通过与使用管道类似的方法来使用套接字,但套接字还包含了计算机网络的通信。
Linux网络编程---套接字。
套接字(socket)是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。套接字的创建和使用与管道是有区别的,因为套接字明确的将客户和服务器区分开来。套接字机制可以实现将多个客户连接到一个服务器。
知识储配:
端口号(port):是传输层协议的内容。
1.端口号是一个2字节16位的整数;
2.端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪个进程来处理;
3.IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
4.一个端口号通常只能被一个进程占用。(特例:父子进程)
5.一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。
6.端口号与主机中表示进程的pid不同,一个是网络上的,后者是操作系统内核中分配的。
字节序:
字节序分为主机字节序和网络字节序:
网络字节序:是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian(大端)排序方式。
主机字节序:不同的主机具有不同的字节序:可以同过以下方法测试自己系统的主机字节序。
//小端字节序:小小小--- 低位放在低地址处为小端
int main()
{
union tst
{
short a;
char b;
}tst;
tst.a = 0x0102;
if(tst.b == 1)
{
printf("big endian\n");
}
else{
printf("little endian\n");
}
return 0;
}
当格式化的数据在两个使用不同字节序的主机之间直接传递时,接收端必然会错误的解释数据,因此,发送端在发送的时候讲数据转化成网络字节序再发送,接收方将数据转成字节的主机字节序接收和解释。需要指出的是,即使在同一台机器的两个进程,有时也要考虑字节序的问题。Linux提供了4个函数来完成主机字节序和网络字节序之间的转换。
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表示长(long)整数
s表示短(short)整数。
地址转换函数:
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
inet_addr:将点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址
inet_aton:同inet_addr功能相同,但是将转化结果存储在参数inp指向的地址结构中。成功返回1,失败返回0.
inet_ntoa:将网络字节序整数表示成IPv4地址转化为用点分十进制字符串表示的IPv4地址,但该函数内部用一个静态变量存储转化结果,函数的返回值指向该竞态内存,因此该函数是不可重入的。在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。
从TCP了解套接字接口函数:
1.套接字描述符:socket打开一个网络通讯端口,如果成功的话,就像open一样返回一个文件描述符。应用程序可以像读写文件一样用read/write在网络上收发数据。
int socket(int domain, int type, int protocol);
domain(域):确定通信的特性,包括地址格式,各个域有自己的格式表示地址,表示各个域的常数都以AF_开头,指地址族。
type:确定套接字类型,进一步确认通信特征。
protocol:通常为0,表示按给定的域和套接字类型选择默认协议。
返回值:
成功: 非负的文件描述符 。 失败:-1
2.将套接字与地址绑定(主要针对服务器,需要绑定一个众所周知的端口,而不能让内核去临时的分配):
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
对于绑定的地址的限制:
1.所运行机器上的指定的地址必须有效
2.地址必须和创建套接字时的地址族所支持的格式相匹配。
3.端口号必须大于1024,除非该进程具有超级特权
4.一般只有套接字端点能够与地址绑定
socket:要绑定的套接字
address:一个通用的指针类型,可以接受多种协议的sockaddr结构体,而他们的长度各不相同,所以需要第三个参数指定结构体的长度。
address_len:地址指针的长度
返回值:成功返回0,出错返回-1
3.监听socket
int listen(int s, int backlog);
返回值:成功返回0,出错返回-1
listenb仅被TCP服务器调用时,它作了两件事:
1.当函数socket创建一个套接字接口时,它被假设为一个主动地套接口,
当有客户端通过connect发起链接这个端口时,listen函数指示内核接受
指向此套接口的请求。调用listen函数将导致套接口从CLOSED状态转为LISTEN状态。
2.函数的第二个参数规定了内核为此套接口排队的最大 链接个数。(典型值为5)
backlog理解(这里可以类比的记忆海底捞吃饭的例子):对于给定的监听套接字,内核维护了两个队列:
未完成链接队列:已由客户端发出并到达服务器,服务器正在等待完成相应的TCP三次握手过程。这些套接口都处于SYN_RCVD状态
已完成连接队列:唯为每个已完成TCP三次握手的客户端请求分配一个条目,这些套接口都处于ESTABLISTED状态。
内核版本2.2之后backlog只表示处于完全连接状态的socket的上限。
4.接受链接
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
accept由TCP服务器调用,从已完成连接队列头返回下一个已完成的链接,若已完成链接队列为空,则accept会阻塞直到一个请求到来,如若sockfd处于非阻塞模式,accept会返回-1并将errno设置为EAGAIN或EWOULDBLOCK。
返回值:成功返回文件描述符,出错返回-1
addr和addrlen用来返回对端的进程(客户端)的协议地址,addrlen是结果参数调用前,我们将由*addrlen所指的整数值置为由cliaddr所指向的套接口地址结构的长度,返回时,这个整数值即为有内核存在此套接口地址结构内的准确字节数。
1>三次握手完成后,服务器调用accept()接受链接
2>addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
3>如果给addr 参数传NULL,表示不关心客户端的地址;
4>addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度
5.客户端链接函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值:成功0,出错-1
sockfd:由socket函数返回的套接口描述字
addr:指向套接口地址结构的指针
addrlen:地址结构的大小
客户端在调用connect前不必非得调用函数bind,因为如若必要的话内核会选择源IP地址和一个临时的端口。如果是TCP套接口的话,函数connect激发TCP的三次握手过程。
客户端和服务器建立连接的函数介绍完毕,下面在介绍几个在数据传输时用到的函数。
如果连接建立成功,那么我们就可以使用read/write来通过套接字进行通信。但是这两个函数的功能有些单一,如果想指定选项从多个客户端接收数据包,或发送外带数据,read/write就不能满足我们的要求。
发送数据:send, sendto, 和 sendmsg 用于向另一个套接字传递消息. Send 仅仅用于连接套接字,而 sendto 和sendmsg 可用于任何情况下.它们的返回值都是成功返回发送的字节数,出错返回-1。
int send(int s, const void *msg, size_t len, int flags);
send和write很像,但是可以指定标志位来改变处理传输数据的方式。
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
send和send类似,区别在于sendto允许在无连接的套接字上指定一个目标地址。
对于面向链接的套接字,目标地址是忽略的,因为目标地址蕴含在连接中,对于无连接的套接字,不能使用send除非在调用connect时预先设定了目标地址,或者采用sendto来提供另一种发送报文的方式。
int sendmsg(int s, const struct msghdr *msg, int flags);
sendmsg提供了一个结构体来存储要发送的数据。
接收数据:返回值都是成功返回以字节计数的消息长度,若无可用消息或对方已经按序结束则返回0,出错返回-1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv和read很像,但是可以指定flags选项来控制如何接收数据。如果发送者已经调用shutdown来结束传输,或者网络协议支持默认的顺序关闭并且发送端已经关闭,那么当所有的数据接收完毕后,recv返回0.
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
如果需要定位数据的发送者,可以使用recvfrom来得到数据发送者的源地址,如果addr非空,它将包含数据发送者的套接字端点地址。当调用recvfrom时,需要设置addrlen参数指向一个包含addr所指的套接字缓冲区字节大小的整数。返回时,改正数设为改地址的实际字节大小因为可以获得发送者的地址,recvfrom通常用于无连接套接字。
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
为了将收到的数据送人多个缓冲区,或者想接受辅助数据可以使用recvmsg。