Linux网络编程
在学习网络编程之前,先了解一下OSI模型,以及TCP/IP协议和一些基础的知识
OSI模型(Open System Interconnection model 开放系统互联模型)
这是一个理想化的模型,实际上的TCP/IP协议跟这个模型还不太一样。
分别简单的理解一下这七层模型的意思
物理层:以二进制数据形式在物理媒体上传输数据,也就是最底层的硬件。
比如各种接口 RS-232 RJ-45 都属于该层
数据链路层:通过物理网络链路提供数据传输,可以理解为硬件和硬件之间的通信
比如IIC SPI就是属于该层
网络层:负责在源和终点之间建立连接,一般包括网络寻径,选择路由
传输层:提供端对端的网络数据流服务。后面将提到的TCP/IP协议也是属于这两层
会话层:解除或建立与别的接点的联系
表示层:提供多种功能用于应用层数据编码和转化,以确保应用层发送的消息能被其他人识别
包括数据格式转化和加密 压缩。
应用层:这里的应用层并非我们pc上所使用的软件,而是向应用服务提供网络资源的API
在TCP/IP协议中,并没有完全照般OSI模型,而是对他进行了整合和拆分
一般我们使用的是一个比较通用的四层的模型
和刚才的模型对比一下:
在实际使用中,我们把他分为:应用层、传输层、网络层、和网络接口层(硬件层)
网络接口层:提供TCP/IP协议的数据结构和实际的硬件之间的接口
网络层:IP协议、RIP协议,负责数据包装 寻址和路由
传输层:TCP可靠的数据流运输服务 UDP不可靠的数据报服务
应用层:FTP文件传输协议、HTTP超文本传输协议、Telent远程终端协议、等
IP地址
Internet Protocol Address 互联网协议地址
每台联网的设备都有一个IP地址,IP地址是一个32位数,常被分成4个4字节的数
ip地址被分为下面几类:
a.b.c.d 的形式,其中abcd都是 0-255的整数
那么问题就来了,一串这样的整数,早晚有一天会用完,毕竟255*255*255*255 = 4228250625
40亿,且不说因为格式等方面的问题,实际分配出去不足40亿。。
光是目前的手机电脑等能联网的设备,也肯定超出了40亿了。
而事实上在2011年IPV4的地址就已经用尽了。
为了解决这个问题,就诞生了IPV6。
IPV6也就是第六版的互联网协议。其地址长度为128位,比32位的IPV4拥有更多的可分配资源。
我们可以使用 ifconfig 查看机器的IP地址
端口号
前面说的IP地址是你计算机的地址,你一台计算机里面肯定运行了很多程序。
每个应用程序就对应了不同的端口号
同时制定了正确的IP地址和端口号,才能准确的发送数据
UNIX操作系统因具有运行稳定、系统要求低、安全性高,而得到广泛应用。其伯克利套接字,发展较早,具有鲜明特点,例如:UNIX系统有保留端口号的概念。只有具有超级用户特权的进程才允许给它自己分配一个保留端口号,这些端口号介于1~1023之间,一些应用程序将它作为客户与服务器之间身份认证的一部分。大多数TCP/IP实现给临时端口分配1024~5000之间的端口号。大于5000的端口与是为其他服务器预留的(Internet上并不常用的服务) 。
TCP、UDP协议
TCP Transmission Control Protocol 传输控制协议
UDP User Datagram Protocol 用户数据报协议
TCP和UDP都可以使用IPV4或IPV6。
UDP不保证数据包会到达目的地,不保证数据先后顺序跨网络之后保持不变,也不保证只到达一次。
TCP也不能保证发送的数据一定会被对方端接收,但是他会不断的重试。
TCP在连接时需要进行三次握手,前面的博客有动图演示讲解
形象一点说,就是这样一个过程:
1、屌丝(客户端)处于自闭状态,女神(服务器)处于等待状态
2、屌丝问女神”我要跟你处对象,是否同意“,同时屌丝变成发送状态
3、女神收到消息,回复屌丝“我同意,那么我要跟你处对象,你是否同意?”,然后变为接收状态
4、屌丝收到消息,变为“恋爱状态”,回复女神“我同意,开始处对象吧”
5、女神收到消息,也变为恋爱状态,于是开始了一段不可描述的故事。。。。
有一天。。。他们要分手了。。
1、屌丝付出了自己所有的爱,然后跟女神说“我要跟你分手”,并把自己变为等待状态
2、女神收到爱和消息,回复“同意分手”,然后又把自己所有的爱给了屌丝,又一次确认“我要和你分手”
3、屌丝收到爱和消息,回复“同意分手”,后进入自闭状态。
4、女神收到应答,也进入自闭状态。。。
上面例子中用用下划线标注出来的,就是建立连接的三次握手和数据发送完成后的四次挥手。
下面终于进入了网络编程的正题:套接字API的使用
创建套接字
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); domain: Name Purpose Man page AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) AF_IPX IPX - Novell protocols AF_NETLINK Kernel user interface device netlink(7) AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7) AF_AX25 Amateur radio AX.25 protocol AF_ATMPVC Access to raw ATM PVCs AF_APPLETALK AppleTalk ddp(7) AF_PACKET Low level packet interface packet(7) AF_ALG Interface to kernel crypto API protocol: SOCK_STREAM TCP SOCK_DGRAM UDP SOCK_SEQPACKET 为最大长度固定的数据报提供有序、可靠、基于双向连接的数据传输路径 SOCK_RAW 原始套接字 SOCK_RDM 提供不保证排序的可靠数据报层。
成功返回非负的套接字描述符
失败返回 -1
绑定套接字和服务器地址
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockfd 套接字文件描述符 addr 服务器地址信息 struct sockaddr { sa_family_t sa_family; char sa_data[14]; } 但实际使用中对于IPV4我们常用这个结构 struct sockaddr_in { sa_family_t sin_family; IPV4对应AF_INET u_int16_t sin_port; 端口号 struct in_addr sin_addr; IP地址 }; /* Internet address. */ struct in_addr { u_int32_t s_addr; IP地址 }; addrlen addr的长度 sizeof(struct sockaddr)
成功返回 0 失败返回 -1
主要用与在TCP中的连接
端口号为0时,自动分配一个临时端口
IP地址填入 INADDR_ANY,自动选择通配地址
监听模式
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); sockfd 套接字文件描述符 backlog 监听队列长度(等待连接的客户端的个数)缺省值20
等待客户端连接
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); sockfd 服务器套接字文件描述符 addr 客户端信息地址 addrlen addr的长度
成功返回连接的客户端的套接字文件描述符
失败返回 -1
接收数据
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); //告诉调用者是谁发来的数据 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); //无连接的套接字 ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); sockfd 套接字文件描述符 buf 存放接收的数据 len 期望接收的数据长度 flags 0 src_addr 源机的IP地址端口号 addrlen addr的长度 msg struct iovec { /* Scatter/gather array items */ void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ }; struct msghdr { void *msg_name; /* optional address */ socklen_t msg_namelen; /* size of address */ struct iovec *msg_iov; /* scatter/gather array */ size_t msg_iovlen; /* # elements in msg_iov */ void *msg_control; /* ancillary data, see below */ size_t msg_controllen; /* ancillary data buffer len */ int msg_flags; /* flags on received message */ };
成功返回接到数据的实际长度,失败返回 -1
recvfrom() 在参数中添加了源机的地址返回,可以得到是从哪里接收到的数据地址信息。
如果把此函数的第五个参数置空NULL,则和recv() 函数相同
发送数据
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
send函数参数和recv完全一致,此处不再赘述。
成功返回发送数据的实际长度,失败返回 -1
sendto() 指定地址发送数据,
sendto() recvfrom() 主要用于UDP的连接
客户端连接服务器
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数为服务器地址信息
成功返回 0 失败返回 -1
connect() 函数不阻塞,所以在使用之前要确保服务器已经进入等待连接状态。
除此之外还有一些工具函数,地址格式的转化 大小端的转化
这里就涉及到一个字节序的问题。
比如我现在要存入一个数据 0x12345678.
有两种存入方式(假设地址从左到右变高):
1、| 12 | 34 | 56 | 78 | 这种存入方式称为大端序
2、| 78 | 56 | 34 | 12 | 这种存入方式称为小端序
网络传输中使用的是网络字节序,也就是大端序。
而在ARM或X86上都是小端序,所以要进行一个转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); 将整型变量从主机字节顺序转变成网络字节顺序
uint16_t htons(uint16_t hostshort); 将主机的无符号短整形数转换成网络字节顺序
uint32_t ntohl(uint32_t netlong); 将一个无符号长整形数从网络字节顺序转换为主机字节顺序
uint16_t ntohs(uint16_t netshort); 将一个16位数由网络字节顺序转换为主机字节顺序
h host 主机
n network 网络
使用TCP协议的流程图:
UDP协议流程图:
试用一下:
服务器代码:
/*server.c*/ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <netinet/ip.h> #define SERVERPOST 1234 int main(int argc, char const *argv[]) { struct sockaddr_in serverAddr,clientAddr; int serverSockfd,clientSockfd; char recvBuf[50]; char sendBuf[50]; int ret; socklen_t addrLen; //创建服务器套接字 serverSockfd = socket(AF_INET,SOCK_STREAM,0); if(serverSockfd == -1) { perror("socket"); exit(-1); } printf("server socketfd is %d \n",serverSockfd); //给定端口号和IP地址,绑定套接字 serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVERPOST); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); ret = bind(serverSockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); if(ret == -1) { perror("bind"); exit(-1); } printf("bind successed\n"); //进入监听状态 ret = listen(serverSockfd,2); if(ret == -1) { perror("linsten"); exit(-1); } //接受客户端的连接请求,返回值为客户端套接字描述符,后面就使用这个描述符作为参数传递数据 clientSockfd = accept(serverSockfd,(struct sockaddr*)&clientAddr,&addrLen); if(clientSockfd == -1) { perror("accept"); exit(-1); } printf("connect successed\n"); close(clientSockfd); close(serverSockfd); return 0; }
客户端代码
/*client.c*/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <netinet/ip.h> #define SERVERPOST 1234 int main(int argc, char const *argv[]) { struct sockaddr_in serverAddr,clientAddr; int serverSockfd,clientSockfd; char recvBuf[50]; char sendBuf[50]; int ret; //创建客户端的套接字 clientSockfd = socket(AF_INET,SOCK_STREAM,0); if(clientSockfd == -1) { perror("socket"); exit(-1); } printf("client socketfd is %d \n",clientSockfd); //给定端口号和IP,使用端口号、IP、套接字描述符 连接服务器 serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVERPOST); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); ret = connect(clientSockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); if (ret == -1) { perror("connect"); exit(-1); } printf("connect successed\n"); close(clientSockfd); }
观察得到现象:
然后我们再加入发送接收数据函数
服务器:
/*server.c*/ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <netinet/ip.h> #define SERVERPOST 1234 int main(int argc, char const *argv[]) { struct sockaddr_in serverAddr,clientAddr; int serverSockfd,clientSockfd; char recvBuf[50]; char sendBuf[50]; int ret; socklen_t addrLen; int recvDataLen,sendDataLen; int i = 5; // serverSockfd = socket(AF_INET,SOCK_STREAM,0); if(serverSockfd == -1) { perror("socket"); exit(-1); } printf("server socketfd is %d \n",serverSockfd); // serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVERPOST); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); ret = bind(serverSockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); if(ret == -1) { perror("bind"); exit(-1); } printf("bind successed\n"); // ret = listen(serverSockfd,2); if(ret == -1) { perror("linsten"); exit(-1); } // clientSockfd = accept(serverSockfd,(struct sockaddr*)&clientAddr,&addrLen); if(clientSockfd == -1) { perror("accept"); exit(-1); } printf("connect successed\n"); strcpy(sendBuf,"hello i am server"); while(i>0) { recvDataLen = recv(clientSockfd,recvBuf,sizeof(recvBuf),0); printf("receive data : %s\n", recvBuf); printf("receive data len : %d\n", recvDataLen); printf("-----------------------------------------\n"); sleep(1); sendDataLen = send(clientSockfd,sendBuf,sizeof(sendBuf),0); printf("send data : %s\n", sendBuf); printf("send data len : %d\n", sendDataLen); i--; } close(clientSockfd); close(serverSockfd); return 0; }
客户端:
/*client.c*/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <netinet/ip.h> #define SERVERPOST 1234 int main(int argc, char const *argv[]) { struct sockaddr_in serverAddr,clientAddr; int serverSockfd,clientSockfd; char recvBuf[50]; char sendBuf[50]; int ret; int recvDataLen,sendDataLen; int i = 5; // clientSockfd = socket(AF_INET,SOCK_STREAM,0); if(clientSockfd == -1) { perror("socket"); exit(-1); } printf("client socketfd is %d \n",clientSockfd); // serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVERPOST); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); ret = connect(clientSockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); if (ret == -1) { perror("connect"); exit(-1); } printf("connect successed\n"); strcpy(sendBuf,"hello i am clinet"); while(i > 0) { sendDataLen = send(clientSockfd,sendBuf,sizeof(sendBuf),0); printf("send data : %s\n", sendBuf); printf("send data len : %d\n", sendDataLen); printf("-----------------------------------------\n"); sleep(1); recvDataLen = recv(clientSockfd,recvBuf,sizeof(recvBuf),0); printf("receive data : %s\n", recvBuf); printf("receive data len : %d\n", recvDataLen); i--; } close(clientSockfd); }
观测结果如下:
服务器:
客户端:
这样看来网络通信成功。
套接字的出现 就是为了解决不同设备上不同进程之间的通信。
接下来试一下UDP:
和上面不同的,UDP也有绑定,但是没有监听等待和连接过程,因为他是指定地址发送和接收数据。
那我们改一下刚才的代码。。
服务器:
/*server.c*/ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <netinet/ip.h> #define SERVERPOST 1234 int main(int argc, char const *argv[]) { struct sockaddr_in serverAddr,clientAddr; int serverSockfd,clientSockfd; char recvBuf[50]; char sendBuf[50]; int ret; socklen_t addrLen = sizeof(clientAddr);//**这里注意**必须这样初始化 否则报错 int recvDataLen,sendDataLen; int i = 5; //创建套接字,以UDP的方式 //serverSockfd = socket(AF_INET,SOCK_STREAM,0); serverSockfd = socket(AF_INET,SOCK_DGRAM,0); if(serverSockfd == -1) { perror("socket"); exit(-1); } printf("server socketfd is %d \n",serverSockfd); //绑定,和TCP相同 serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVERPOST); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); ret = bind(serverSockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); if(ret == -1) { perror("bind"); exit(-1); } printf("bind successed\n"); /* //UDP中没有监听和等待连接的过程 所以listen accept 不需要 ret = listen(serverSockfd,2); if(ret == -1) { perror("linsten"); exit(-1); } // clientSockfd = accept(serverSockfd,(struct sockaddr*)&clientAddr,&addrLen); if(clientSockfd == -1) { perror("accept"); exit(-1); } printf("connect successed\n"); */ strcpy(sendBuf,"hello i am server"); while(1) { recvDataLen = recvfrom(serverSockfd,recvBuf,sizeof(recvBuf),0,(struct sockaddr*)&clientAddr,&addrLen); printf("receive data : %s\n", recvBuf); printf("receive data len : %d\n", recvDataLen); printf("-----------------------------------------\n"); sendDataLen = sendto(serverSockfd,sendBuf,sizeof(sendBuf),0,(struct sockaddr*)&clientAddr,addrLen); printf("send data : %s\n", sendBuf); printf("send data len : %d\n", sendDataLen); if (sendDataLen == -1) { perror("sever send"); } } close(clientSockfd); close(serverSockfd); return 0; }
客户端:
/*client.c*/ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <netinet/ip.h> #define SERVERPOST 1234 int main(int argc, char const *argv[]) { struct sockaddr_in serverAddr,clientAddr; int serverSockfd,clientSockfd; char recvBuf[50]; char sendBuf[50]; int ret; int recvDataLen,sendDataLen; int i = 5; socklen_t addrLen = sizeof(serverSockfd);//**注意** 这里必须这样初始化 //创建客户端套接字 //clientSockfd = socket(AF_INET,SOCK_STREAM,0); clientSockfd = socket(AF_INET,SOCK_DGRAM,0); if(clientSockfd == -1) { perror("socket"); exit(-1); } printf("client socketfd is %d \n",clientSockfd); //UDP服务器没有监听等待,所以也不需要客户端发送连接请求,直接发数据过去即可 serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVERPOST); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); /* ret = connect(clientSockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); if (ret == -1) { perror("connect"); exit(-1); } printf("connect successed\n"); */ strcpy(sendBuf,"hello i am clinet"); while(1) { sendDataLen = sendto(clientSockfd,sendBuf,sizeof(sendBuf),0,(struct sockaddr*)&serverAddr,sizeof(serverAddr)); printf("send data : %s\n", sendBuf); printf("send data len : %d\n", sendDataLen); printf("-----------------------------------------\n"); recvDataLen = recvfrom(clientSockfd,recvBuf,sizeof(recvBuf),0,(struct sockaddr*)&serverAddr,&addrLen); printf("receive data : %s\n", recvBuf); printf("receive data len : %d\n", recvDataLen); } close(clientSockfd); }
UDP的连接是通过客户端发送数据,这样服务器接收数据的同时也接到了客户端的地址信息,继而通过这个地址信息进行后面的数据交流。
这里的 sendto 和 recvfrom 函数,在使用时要注意,接收就填写接收端的sockfd,发送就填写发送端的sockfd
后面参数中的地址,recvfrom从谁那里收数据就填谁,sendto数据要发给谁就填谁