网络编程就是编写程序使两台联网的计算机相互交换数据。
那么,这两台计算机之间用什么传输数据呢?首先你肯定先需要物理连接嘛。
在此基础上,只需要考虑如何编写数据传输程序。看似很麻烦,但实际上这点不用愁,因为操作系统已经提供了 socket。即使对网络数据传输的原理不太熟悉,我们也能通过 socket 来编程。
一、什么是socket
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。
通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
socket 的典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
例如我们每天浏览网页、QQ 聊天、收发 email 等等。
二、socket 套接字的分类
这个世界上有很多种套接字,Internet 套接字、Unix套接字、X.25 套接字等。
这里我们讨论 Internet 套接字,它是最具代表性的,也是最经典最常用的。我们经常提及的套接字,基本也都是指 Internet 套接字。
根据数据的传输方式,可以将 Internet 套接字分成两种类型。
通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。
1、流格式套接字(SOCK_STREAM)
流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。
SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
SOCK_STREAM 有以下几个特征:
- 数据在传输过程中不会消失;
- 数据是按照顺序传输的;
- 数据的发送和接收不是同步的(有的说法也称“不存在数据边界”)。
可以将 SOCK_STREAM 想象成一条传输带,只要传输带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。
为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。
你也许见过 TCP,是因为你经常听说“TCP/IP”。TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。
那么,“数据的发送和接收不同步”该如何理解呢?
假设传输带传送的是水果,接收者需要凑齐 100 个后才能装袋,但是传送带可能把这 100 个水果分批传送,比如第一批传送 20 个,第二批传送 50 个,第三批传送 30 个。接收者不需要和传送带保持同步,只要根据自己的节奏来装袋即可,不用管传送带传送了几批,也不用每到一批就装袋一次,可以等到凑够了 100 个水果再装袋。
流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。
也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。
2、数据报格式套接字(SOCK_DGRAM)
数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。
可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:
- 强调快速传输而非传输顺序;
- 传输的数据可能丢失也可能损毁;
- 限制每次传输的数据大小;
- 数据的发送和接收是同步的(也称为“存在数据边界”)。
总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。
数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。
三、socket 服务器和客户端的开发步骤
1.创建套接字
socket()函数
int socket(int domain, int type, int protocol);
domain:
指明所使用的协议族,通常为 AF_INET,表示互联网协议族(TCP/IP 协议族)
AF_INET IPv4因特网域.
AF_INET6 IPv6 因特网域
AF_UNIX Unix 域
AF_ROUTE 路由套接字
AF_KEY 密钥套接字
AF_UNSPEC 未指定
type参数指定 socket 的类型
SOCK_STREAM:
流式套接字提供可靠的、面向连接的通信流,它使用 TCP 协议,从而保证了数据传输的正确性和顺序性
SOCK_DGRAM:
数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用教据报协议UDP
SOCK_RAW: 允许程序使用低层协议,原始套接字允许对底层协议如 IP 或 ICMP 进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发
protocol:
通常赋值"0"
0选择type类型对应的默认协议
IPPROTO_TCP TCP 传输协议
IPPROTO_UDP UDP 传输协议
IPPROTO_SCTP SCTP 传输协议
IPPROTO_TIPC TIPC 传输协议
2.为套接字添加信息 (IP地址和端口号)
bind()函数: IP号端口号与相应描述字赋值函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能
用于绑定 IP 地址和端口号到 socketfd
参数
sockfd
是一个 socket 描述符
addr
是一个指向包含有本机 IP 地址及端口号等信息的 sockaddr 类型的指指向要绑定给 sockfd 的协议地址结构,这个地址结构根据地址创建 socket 时的地址协议族的不同而不同
struct sockaddr{ unisgned short as_family; //协议族 char sa_data[14]; //IP+端口 };
同等替换
struct sockaddr_in { sa_family_t sin_family; //协议族 in_port_t sin_port; //端口号 struct in_addr sin_addr; //IP地址结构体 unsigned char sin_zero[8]; //没有实际意义只是为跟sockaddr结构在内存中对亮这样两者才能相互转换 };
地址转换API
int inet_aton(const char *cp, struct in_addr *inp); 把字符串形式的“192.168.1.123”转为网络能识别的格式
char* inet_ntoa(struct in_addr inaddr); 把网络格式的ip地址转为字符串形式
3.监听网络连接
listen()函数:监听设置函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog);
功能
设置处理的最大连接数,listen()并未开始接受连线,只是设置 sockect 的 listen 模式,listen 函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动此要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接清求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
内核为任何一个给定监听套接字维护两个队列:
(1) 未完成连接队列,每个这样的 SYN 报文段对应其中一项:已由某个客户端发出并到达服务器,而服务正在等待完成相应的 TCP 三次握手过程。这些套接字处于SYN_REVD 状态
(2) 已完成连接队列,每个已完成 TCP 三次握手过程的客户端对应其中一项。这些套接字处于 ESTABLISHED 状态
参数
sockfd
sockfd 是 socket 系统调用返回的服务器端 socket 描述符
backlog
backlog 指定在请求队列中允许的最大请求数
4.监听到有客户端接入,接受一个连接
accept()函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
accept 函数由 TCP 服务器调用,用于从已完成连接队列队头回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
参数
sockfd
sockfd是 socket 系统调用返回的服务器端 socket 描述符
addr
用来返回已连接的对端(客户端)的协议地址
addrled
客户端地址长度
返回值
该函数的返回值是一个新的套接字描术符,返回值是表示已连接的套接字描述待,而第一个参教是服务跟监听套接字描术待。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务进程接受的客户连接创建一个已连接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
5.数据交互(数据收发)
(1)字节流读取函数
在套接字通信中进行字节读取函数:read(),write()。与I/O 中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少
ssize_t write(int fd, const void*buf,size_t nbytes); ssize_t read(int fd,void *buf,size_t nbyte); /*函数均返回读或写的字节个数,出错则返回-1 */
第一个将 buf 中的 nbytes 个字节写入到文性描术符 fd 中,成功时返回写的字节数。第二个为从fd 中读取 nbyte 个字节到 buf 中,返回实际航读的字节数。详细应用说明参考使用 read write 读写 socket(套节字)。网络I/0还有一些函数例如: recv() / send(),readv()/writev(),recvmsg()/ sendmsg(), recvfrom()/ sendto()等
(2)在TCP套接字上发送数据函数:有连接
ssize _t send(int s,const void *msg,size_t len,int flags); //包含3要素: 接字s,待发数据msg,数据长度/en //函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述符,即accept函数的返回值 //参数msg指向存放待发送数据的缓冲区 //参数len为待发送数据的长度,参数flags为控制选项,一般设置为0
(3)在TCP套接字上接收数据函数: 有连接
ssize_t recv(int s,void *buf,size_t len,int flags); //包合3要素:套接字s,接收缓冲区buf,长len //函数recv从参数s所指定的套接字述符(必须是面向连接的套接)上接收数据并保存到参教buf指定的缓冲区 //参数len则为缓冲区长度,参数flags为控制选项一般设置为0
(4) 客户端的connect函数
connect()函数:客户机连接主机
#include <sys/types.h> /* See NOTES*/ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr ,socklen_t addrlen);
功能
该函数用于绑定之后的 client 端(客户端),与服务器建立连接
参数
sockfd
是目的服务器的 sockect 描述符
addr
是服务器端的 IP 地址和端口号的地址结构指针
addrlen
地址长度常被设置为 sizeof(struct sockaddr)返回值
成功返回0,遇到错误时返回 -1,并且 errno 中包含相应的错误码
6.关闭套接字,断开连接