Linux环境:C编程之网络通信
网络通信概述
计算机网络通信内容很多,这只是一个针对socket编程的基础知识概述总结
通信参考模型
互联网通信遵循一定的通信协议和通信机制,称为通信模型。
国际标准化的开放网络通信模型是OSI参考模型,但是OSI模型概念提出较晚,所以一直没有实现,只是停留在概念性的参考框架上。目前通用的模型为TCP/IP通信参考模型,以下主要介绍TCP/IP通信参考模型。
自顶向下的TCP/IP参考模型
应用层
- 客户在应用层使用各种应用和协议进行通信,应用层实现了应用到应用之间的通信,即进程与进程之间的通信,通过端口识别不同的进程。
- 常用的应用层协议包括http 超文本传输协议、ftp 文件传输协议 、telnet 远程登录 、ssh 安全外壳协议 、stmp 简单邮件发送协议、pop3 邮件收发协议。应用层程序员可以通过编程实现自己的通信协议。
- 应用层的通信协议规定了用户传输的数据信息的格式,并附加了相关的格式控制信息等,称为报文。
- 应用层的报文传输由传输层实现。
传输层
- 传输层负责实现端到端通信,即主机到主机之间的通信,源主机ip到目标主机ip
- 传输层把应用层的报文按照大小划分为报文段并附加传输层协议的格式控制信息
- 报文段由传输层交付网络层传输。
- 传输层从协议主要是TCP协议和UDP协议
- TCP协议通过三次握手(-- 在吗?-- 我在,你在吗?-- 我也在,我们开始吧),四次挥手(–我要走了 – 好的我知道了你走吧 – 我也要走了 – 好的再见)机制实现端到端的可靠连接,通过给报文加序号实现乱序重排,并通过滑动窗口协议实现出错重传
- UDP协议属于无连接协议,不可靠,但是实时性高,只负责发送数据,不负责顺序发送和出错重传。
网络层
- 网络层为传输层提供无连接的,尽最大努力交付的数据报服务,主要由IP协议,ICMP,IGMP协议和一系列路由方法协议组成。数据的可靠性需要传输层自己保证。
- 网络层协议负责将传输层的报文段封装成ip分组(数据包),并实现路由寻径,找到一条从源主机到目的主机的通路。
- 具体的数据流传输控制由链路层进行。
链路层
- 链路层协议实现具体的二进制数据流传输,对上层掩盖了物理传输的具体细节,是TCP/IP模型的最底层
- 链路层协议包括ARP,RARP等协议,实现了ip地址和物理mac地址的对应
- 链路层根据物理网络状况将ip分组划分封装成数据帧进行传输,负责数据差错的检查和校验
socket编程预备知识
socket概述
- socket是操作系统内核中的一种数据结构,用来在同一主机或不同主机的进程通信中标识不同进程。
- Linux系统中的socket是一种特殊的I/O接口,同样由文件描述符进行管理,属于特殊类型的文件。 ,
- 每一个 socket 都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。
- 创建一个socket时只需要指明其通信域和通信协议类型即可
- 通信域规定了socket的地址格式和通信范围
socket类型
- 流式 socket(
SOCK_STREAM
):用于 TCP 通信 - 数据报 socket(
SOCK_DGRAM
) :用于 UDP 通信 - 原始 socket(
SOCK_RAW
) :用于新的网络协议实现的测试等,可以直接访问网络层协议
socket地址类型
socke创建后需要绑定通信地址才能使用,socket的地址格式有两种如下:两种地址可以相互转化
1、sockaddr通用地址
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。*/
};
sockaddr
缺陷:sa_data
把目标地址和端口信息混在一起,不利于网络通信
2、sockaddr_in网络地址
struct sockaddr_in
{
short int sin_family; /*地址族*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP 地址*/
unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/
};
//保存32位二进制ip地址的结构体
struct in_addr
{
unsigned long int s_addr; /* 32 位 IPv4 地址,网络字节序 */
};
数据存储字节序——大端小端
- 计算机中的数据按字节存储,但是有很多数据常常占据多个字节,这种情况下就需要考虑字节的存储顺序,就像人们的书写顺序一样。
- 如果先存高位,再存低位,即高位字节对应的内存地址编号小,从地址0读数据先读到的是高位时,称为大端模式。大端模式同样也符合人们的书写阅读习惯,从左到右先写高位再写低位,因此互联网通信中采用的都是大端模式
- 如果先存低位,再存高位,即低位字节对应的内存地址编号小,就称为小端模式。主机中的字节序和处理器相关,市面上的主流处理器如intel都是小端模式。可以通过以下程序判断主机是大端还是小端模式。
#include <stdio.h> int main() { int x=0x12345678; //占四个字节,如果是大端的话从低地址到高地址依次是0x12,0x34,0x56,0x78 char * p=(char *)&x; printf("%0x %0x %0x %0x\n",p[0],p[1],p[2],p[3]); //从低位到高位按字节输出,如果是小端会得到:78,56,34,12 }
- 由于主机字节序和网络字节序常常有冲突,因此需要进行字节序的转换,才能正常进行网络通信,这里用到四个函数:
uint16_t htons(uint16_t host16bit)
:host to net short 的缩写,将16位的主机字节序转换为16位的网络字节序并返回,用于16位端口号从主机到网络的转换uint16_t ntohs(uint16_t net16bit)
:net to host short 的缩写,将16位的网络字节序转换为16位的主机字节序并返回,用于16位端口号从网络到主机的转换uint32_t htonl(uint32_t host32bit)
:host to net long 的缩写,将32位的主机字节序转换为32位的网络字节序并返回,用于32位ip地址的转换uint32_t ntohl(uint32_t net32bit)
:net to host long 的缩写,将32位的网络字节序转换为32位的主机字节序并返回,用于32位ip地址的转换
ip地址格式转换
通常用户在表达ip地址时采用的是点分十进制或者是冒号分开的十进制 Ipv6 地址。而在socket 编程中使用的则是 32 位的网络字节序的二进制值,这就需要将这两个数值进行转换,转换用到如下函数:
只适用于ipv4的函数:
int inet_aton(const char *straddr, struct in_addr *addrptr);
将点分十进制数的 IP 地址(straddr
)转换成为网络字节序的 32 位二进制数值(addrptr
),成功,则返回 1,不成功返回 0char *inet_ntoa(struct in_addr inaddr);
将网络字节序的 32 位二进制数值inaddr
转换为点分十进制的 IP 地址返回值unsigned long int inet_addr(const char *straddr);
将点分十进制数的 IP 地址(straddr
)转换成为网络字节序的 32 位二进制数值返回值
兼容ipv6的函数:int inet_pton(int family, const char *src, void *dst);
将ip地址src转为网络字节序的二进制数值,family 参数指定为 AF_INET,表示是 IPv4 协议,如果是 AF_INET6,表示 IPv6 协议const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);
将二进制数值src转为ip地址dst,len表示转换后的字符串长度
ip地址和域名(主机名)之间的转换
ip地址不方便记忆,因此实际用户访问网络时通过域名或主机名(局域网)访问,所以还需要域名(主机名)和ip地址的转换,准确的说是通过ip或者主机名获取到主机信息。
主机信息用结构体存放定义如下:
struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*主机 IP 地址类型 IPv4 为 AF_INET*/
int h_length; /*主机 IP 地址字节长度,对于 IPv4 是 4 字节,即 32 位*/
char **h_addr_list; /*二进制表示的主机的 IP 地址列表,一个域名或主机可能对应多个ip*/
}
struct hostent* gethostbyname(const char* hostname);
参数为主机名,返回主机信息结构体指针,指针为NULL表示查找失败struct hostent* gethostbyaddr(const char* addr,size_t len,int family);
参数为ip地址的二进制表示,len为地址长度,family为地址类型
socket编程
TCP通信协议编程
TCP 协议 socket 通信过程:
服务端:socket—bind—listen—while(1){—accept—recv—send—close—}---close
客户端:socket----------------------------------connect—send—recv-----------------close
服务器端创建socket,用bind绑定服务器的ip和对应进程的端口号,然后通过listen函数注册为被动socket,等待客户端进程的主动连接,通过rev接收消息,send发送消息,close关闭连接,然后继续监听等待连接
客户端创建socket,调用connect主动连接到一个被动socket,发送数据,接收回应,然后关闭连接
socket函数
- 作用:创建一个socket,返回其文件描述符
- 原型:
int socket(int domain,int type,int protocol);
- 参数:
domain
:通信域,AF_INET
:Ipv4 网络协议 ,AF_INET6
:IPv6 网络协议,AF_UNIX
:内核通信type
:SOCK_STREAM
:TCP协议,流式socket;SOCK_DGRAM
:UDP协议,数据包socketprotocol
:指定 socket 所使用的传输协议编号。通常为 0
- 返回值:成功则返回socket描述符,失败返回-1
bind函数
- 作用:将一个socket与一个地址结构绑定,使其与指定的端口号和 IP 地址相关联
- 原型:
int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
- 参数:
sockfd
:socket文件描述符sockaddr
:地址结构指针,结构体类型和地址的类型相关addrlen
:sizeof(struct sockaddr),地址结构体的长度
- 返回值:成功则返回 0,失败返回-1
listen 函数
- 作用:listen函数将一个socket注册为被动socket,用来监听等待其他socket的主动connect。一个成功connect的socket无法再注册为被动socket
- 原型:
int listen(int sockfd,int backlog);
- 参数:
sockfd
为套接字描述符,backlog
可以理解为该socket同时能处理的最大连接要求,通常为 5 或者 10,最大值可设至 128.。 - 返回值:成功则返回 0,失败返回-1
accept函数
- 作用:在监听socket上接受一个调用了connect的主动socket的连接,如果不存在接入连接则阻塞等待连接。调用accept成功后会创建一个新的socket与发起连接的socekt建立连接并进行后续通信。
- 原型:
int accept(int sfd,struct sockaddr * addr,int * addrlen);
- 参数:
sfd
:传入参数,socket描述符,服务端的socket描述符addr
:传出参数,创建的新socket的地址结构体指针,如果不需要获取该socket地址时可以传入NULL。addrlen
:调用时填addr指针指向的缓冲区大小,传出时变为addr指向的结构体实际占用的大小
- 返回值:成功则返回 创建的新socket的文件描述符,失败返回-1
connect函数
- 作用:用来请求连接一个指定ip和端口号的处于监听状态的被动socket
- 原型:
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
- 参数:
sockfd
:申请连接的主动socket的文件描述符serv_addr
:为结构体指针变量,存储着服务端被动socket的 IP 与端口号信息。addrlen
:表示serv_addr指向的结构体变量的长度
- 返回值:成功则返回 0,失败返回-1
I/O函数
socket是文件类型,因此可以使用read和write函数进行读写。
但是,由于socket用于网络通信,因此在使用read和write函数时可能会出现一些问题。
比如write函数只负责将数据写入本地socket的缓冲区中,并不保证全部数据都能写入,可能会由于网络连接中断等原因导致数据没有完全写入,需要自行控制重新写入,
read函数虽然可以读取指定长度的字符,但是可能对方的信息还没有完全发送完毕,这时候可能只读了一部分数据。
为了解决这些问题,在socket通信中使用recv和send函数来进行读写,在这两个函数中多了控制信息参数。
recv函数
-
作用:用新的套接字来接收远端主机传来的数据,并把数据存到由参数 buf 指向的内存空间
-
原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
-
参数:
sockfd
:接收方socket的文件描述符buf
:指向接收数据的缓冲区len
:表示缓冲区的长度,即指定的接收长度flags
:0,此时和read函数没有区别;MSG_PEEK:表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.;MSG_WAITALL表示等到所有的信息到达时才返回.使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误.
-
返回值:成功则返回实际接收到的字符数,可能会少于指定的接收长度(读到了文件结尾)。失败返回-1。
send函数
- 作用:使用socket发送数据
- 原型:
int send(int sfd,const void * msg,int len,unsigned int flags);
- 参数:前三个参数和write函数一致,sfd是发送方socket的文件描述符,重点在
flags
的取值:- flags=0:和write函数功能一致。默认为阻塞模式,即缓冲区有数据未发送时会阻塞等待
- flags=MSG_DONTWAIT:send以非阻塞方式运行,发送缓冲区被占用时立即返回
- flags=MSG_NOSIGNAL:send发送时对方关闭连接,如果指定该标志量则发送进程不会收到SIGPIPE信号。
- 返回值:成功则返回实际传送出去的字符数,可能会少于指定的发送长度。失败返回-1。
- 如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到SIGPIPE信号,进程对该信号的默认处理是进程终止
close函数
socket同样是文件,故只需调用close关闭文件的方式关闭即可。
需要注意的是,调用close关闭套接字时,双向通信的两端都会关闭,不能读也不能写。
shutdown函数
- 作用:可以只关闭一端的连接,数据还可以在一个方向上传输。同时,关闭的时候,所有的文件描述符都会失效。
- 原型:
int shutdown(int sockfd,int how)
: - 参数:
socket:
文件描述符how
:SHUT_RD
:关闭读端 ,之后再进行读操作返回文件结尾。对等端进行写操作则会收到SIGPIPE信号,继续写会报EPIPE错误。SHUT_WR
:关闭写端。对等端读操作的时候会读到文件结尾,本地写操作将产生SIGPIPE信号和EPIPE错误。SHUT_RDWR
:关闭读写端
- 返回值:成功返回0,失败返回-1。
UDP通信协议 编程
UDP协议协议 socket 通信过程:
服务端:socket—bind—recvfrom—sendto—close
客户端:socket----------sendto—recvfrom—close
UDP通信类似收发快递:
服务端创建数据报socket,相当于创建快递柜,调用recvfrom函数来接收数据报,没有则阻塞。sendto相当于发快递,需要指明接收方的地址才能发送出去。
recvfrom函数
- 作用:接收一个数据报socket传来的数据报,并可以获得发送方的地址
- 原型:
int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
- 参数:
- 前四个参数和
recv
函数一致, from
参数是传出参数,传出发送方的地址结构体指针fromlen
参数也是传出参数,标识了from
的长度
- 前四个参数和
- 返回值:成功则返回实际接收到的字符数,可能会少于指定的接收长度(读到了文件结尾)。失败返回-1。
sendto函数
- 作用:向指定地址的socket发送数据报
- 原型:
int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
- 参数:
- 前四个参数和
send
函数一致, to
参数指定接收方的地址结构体tolen
参数标识了to
指向的结构体的长度
- 前四个参数和
- 返回值:成功则返回实际传送出去的字符数,可能会少于指定的发送长度。失败返回-1。