一 什么是套接字Socket
1.Socket简介
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口 。
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制 。
2.Socket的域(domain)
域指定套接字通信中使用的网络介质。最常见的套接字域是 AF_INET(IPv4)或者AF_INET6(IPV6),它是指 Internet 网络,许多 Linux 局域网使用的都是该网络,当然,因特网自身用的也是它。
3.Socket主要类型(type)
- 流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 。 - 数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。 - 原始套接字(SOCK_RAW)
原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接 。
4.Socket基本工作流程
要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket 。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤 :
- 服务器监听
所谓服务器监听,是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态 。 - 客户端请求
所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端接字提出连接请求 。 - 连接确认
所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求 。
二 创建套接字Socket
1.socket函数
int socket(int domain, int type, int protocol);
三 绑定套接字Socket与主机网络地址
1.bind函数
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
2.struct sockaddr与struct sockaddr_in
typedef unsigned short int sa_family_t;
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr {
in_addr_t s_addr;
};
- 这两个结构体一样大,都是16个字节,而且都有family属性,二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。不同的是:
sockaddr结构体中sa_data成员融合了端口与地址信息,而sockaddr_in结构体用两个成员sin_port和sin_addr分别表示端口号和地址信息 - sin_port和sin_addr都必须是网络字节序(NBO Network byte order),一般可视化的数字都是主机字节序(HBO Host byte order),下文详解。
- sockaddr是给操作系统用的。程序员应使用sockaddr_in来表示地址,把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数。sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
示例:
int sockfd;
struct sockaddr_in serverAddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERV_PORT);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(sockfd, (struct sockaddr *) &serverAddr, sizeof(serverAddr));
3.常用填充地址信息的方法
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_aton("127.0.0.1",&serverAddr.sin_addr);
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = htons(1234);
serverAddr.sin_port = htons(0);
相关函数: 1. inet_addr
in_addr_t inet_addr(const char *cp);
2.inet_ntoa 、inet_aton
char *inet_ntoa (struct in_addr in)
int inet_aton(const char *cp, struct in_addr *inp);
4.htons、htonl
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
5.inet_pton、inet_ntop 这两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构的二进制值。
int inet_pton(int family, const char *strptr, void *addrptr);
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
4.主机字节序与网络字节序
NBO : 网络字节序 HBO : 主机字节序 LE little-endian:小端 BE big-endian:大端
- 网络字节序和主机字节序:
网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址。 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,为了不使数据流乱序,接收主机也会把从网络上接收的数据按内存地址从低到高的顺序保存在接收缓冲区中。 TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
tcp/ip规定它们的网络字节序都是大端字节序。主机字节序可能是大端也可能是小端,与主机的cpu有关,与操作系统无关考虑到与协议的一致以及与同类其它平台产品的互通,在程序中发数据包时,将主机字节序转换为网络字节序,收数据包处将网络字 节序转换为主机字节序。网络程序开发时 或是跨平台开发时 应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug。数据在传输的过程中,一定有一个标准化的过程,也就是说: 从主机a到主机b进行通信:a的主机字节序——网络字节序——b的主机字节序
- 大端字节序和小端字节序:
大端字节序存储时值的高位存储在较小的地址,值的低位存储在较大的地址。 小端字节序存储时值的高位存储在较大的地址,值的低位存储在较小的地址。 以0x12345678为例: 地址:0x1000 ?0x1001? 0x1002 ?0x1003 小端: 78 ???56 ???34 ???12 大端: 12 ???34 ???56 ???78 - 测试主机是大端还是小端的方法:
int main()
{
union
{
short s;
char c[sizeof(short)];
}un;
un.s = 0x0102;
if(sizeof(short)==2)
{
if(un.c[0] == 1 && un.c[1] == 2)
printf("Big-Endian\n");
else if(un.c[0] == 2 && un.c[1] == 1)
printf("Little-Endian\n");
else
printf("Unknown\n");
}
else
print("sizeof(short)=%d\n",sizeof(short));
exit(0);
}
四 UDP通信的实现
在创建并绑定套接字之后,我们就可以尝试TCP、UDP通信了。 TCP/IP协议是一个协议簇。里面包括很多协议,UDP只是其中的一个。
- UDP(User Datagram Protocol用户数据报协议)是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
- UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。
1.recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
2.sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
3.示例
实现服务器端与客户端聊天 运行效果:
#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
char buf_data[1024] = {};
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd:%d\n",sockfd);
struct sockaddr_in myselfAddr;
myselfAddr.sin_family = AF_INET;
myselfAddr.sin_port = htons(6666);
myselfAddr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret_bind = bind(sockfd,(struct sockaddr*)&myselfAddr,sizeof(myselfAddr));
if(ret_bind == -1)
{
perror("bind");
close(sockfd);
exit(1);
}
struct sockaddr_in buf_sockaddr;
socklen_t buf_addrlen = sizeof(buf_sockaddr);
printf("等待客户端连接...\n");
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
pid_t pid = fork();
if(pid>0)
{
while(1)
{
bzero(buf_data,sizeof(buf_data));
gets(buf_data);
ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&buf_sockaddr,buf_addrlen);
if(ret_send == -1)
{
perror("sendto");
close(sockfd);
exit(1);
}
printf("我:%s\n",buf_data);
}
}
else if(pid == 0)
{
while(1)
{
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
}
}
else
{
perror("fork");
close(sockfd);
exit(1);
}
return 0;
}
#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
char buf_data[1024] = "\0";
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket");
exit(1);
}
printf("sockfd:%d\n",sockfd);
short port;
char IP[20];
printf("输入对方IP:\n");
scanf("%s",IP);
getchar();
printf("输入对方端口号:\n");
scanf("%hd",&port);
getchar();
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port);
serverAddr.sin_addr.s_addr = inet_addr(IP);
struct sockaddr_in buf_sockaddr;
socklen_t buf_addrlen = sizeof(buf_sockaddr);
pid_t pid = fork();
if(pid>0)
{
while(1)
{
bzero(buf_data,sizeof(buf_data));
gets(buf_data);
ssize_t ret_send = sendto(sockfd,buf_data,strlen(buf_data)+1,0,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
if(ret_send == -1)
{
perror("sendto");
close(sockfd);
exit(1);
}
printf("我:%s\n",buf_data);
bzero(buf_data,sizeof(buf_data));
}
}
else if(pid == 0)
{
while(1)
{
bzero(buf_data,sizeof(buf_data));
ssize_t ret_recv = recvfrom(sockfd,buf_data,sizeof(buf_data),0,(struct sockaddr*)&buf_sockaddr,&buf_addrlen);
if(ret_recv == -1)
{
perror("recvfrom");
close(sockfd);
exit(1);
}
printf("IP:%s:%s\n",inet_ntoa(buf_sockaddr.sin_addr),buf_data);
}
}
else
{
perror("fork");
close(sockfd);
exit(1);
}
return 0;
}
五 TCP通信的实现
- TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。
- TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。
1.listen函数(server端)
- 对于服务器端程序,使用bind 函数绑定套接字后,还需要使用listen 函数让套接字进入被动监听状态,再调用accept 函数,就可以随时响应客户端的请求了。
- 所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
int listen(int sockfd, int backlog);
- 当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为 请求队列(Request Queue)
- 当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误
- listen只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept函数
2.accept函数(server端)
- 当套接字处于监听状态时,可以通过 accept函数来接收客户端请求。
- listen只是让套接字进入监听状态,并没有真正接收客户端请求,listen后面的代码会继续执行,直到遇到 accept
- accept 会阻塞程序执行,直到有新的请求到来。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
3.connect函数(client端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
4.write与read函数
建立好了 TCP 连接之后,我们就可以把得到的 sockfd 当作文件描述符来使用。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
需要注意 read 函数的返回值:
- retval > 0 :实际读到的字节数
- retval = 0 :
?????普通文件 — 到达文件末尾 ?????管道文件 — 管道写端关闭 ?????套接字文件 — 对端关闭,网络断开 - retval < 0 :出错
5.send与recv函数
recv 和 send 函数提供了和 read 和 write 差不多的功能。前3个参数同read、write,第4 个参数用来控制读写操作。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
6.示例
实现客户端与服务器端的对话,服务器端运行时命令行传参端口号,客户端运行时命令行传参服务器的IP和端口号 关于程序中用到的IO多路复用select函数,参考select函数详解
运行效果:
#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
void error_Handling(char* func,int retval);
void error_of_read(int retval,char* IP);
void sigFun(int sig);
int count = 0;
int main(int argc,char* argv[])
{
if(argc != 2)
{
printf("%s Port",argv[0]);
exit(1);
}
signal(SIGCHLD,sigFun);
char buf_data[1024] = {};
int listenfd = socket(AF_INET,SOCK_STREAM,0);
error_Handling("socket",listenfd);
int on = 1;
int ret_set = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
error_Handling("setsockopt",ret_set);
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(atoi(argv[1]));
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret_bind = bind(listenfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
error_Handling("bind",ret_bind);
int ret_listen = listen(listenfd,10);
error_Handling("listen",ret_listen);
struct sockaddr_in buf_addr;
socklen_t buf_addrlen = sizeof(buf_addr);
while(1)
{
printf("服务器持续监听中\n");
printf("连接服务器的客户端数量:%d\n",count++);
int newconfd = accept(listenfd,(struct sockaddr*)&buf_addr,&buf_addrlen);
error_Handling("accept",newconfd);
pid_t pid = fork();
error_Handling("fork",pid);
if(pid == 0)
{
printf("FATHERPID:%d CHILDPID:%d\n",getppid(),getpid());
printf("与IP:| %s |建立连接\n",inet_ntoa(buf_addr.sin_addr));
while(1)
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(0,&readfds);
FD_SET(newconfd,&readfds);
int ret_select = select(newconfd+1,&readfds,NULL,NULL,NULL);
error_Handling("select",ret_select);
if(FD_ISSET(0,&readfds))
{
ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));
error_Handling("read",ret_read);
ssize_t ret_write = write(newconfd,&buf_data,sizeof(buf_data));
error_Handling("write",ret_write);
printf("我:%s\n",buf_data);
bzero(&buf_data,sizeof(buf_data));
}
if(FD_ISSET(newconfd,&readfds))
{
ssize_t ret_read = read(newconfd,&buf_data,sizeof(buf_data));
error_of_read(ret_read,inet_ntoa(buf_addr.sin_addr));
printf("%s:%s\n",inet_ntoa(buf_addr.sin_addr),buf_data);
bzero(&buf_data,sizeof(buf_data));
}
}
}
}
close(listenfd);
return 0;
}
void error_Handling(char* func,int retval)
{
if(retval == -1)
{
perror(func);
exit(1);
}
}
void error_of_read(int retval,char* IP)
{
if(retval<0)
{
perror("read");
exit(1);
}
if(retval == 0)
{
perror("read");
printf("%s 已断开连接,此进程结束\n",IP);
exit(1);
}
}
void sigFun(int sig)
{
wait(NULL);
count--;
printf("有子进程退出已经收尸\n");
}
#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
void error_Handling(char* func,int retval);
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("%s IP Port\n",argv[0]);
exit(1);
}
char buf_data[1024] = {};
int sockfd = socket(AF_INET,SOCK_STREAM,0);
error_Handling("socket",sockfd);
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(atoi(argv[2]));
serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
int ret_connect = connect(sockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
error_Handling("connect",ret_connect);
while(1)
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(0,&readfds);
FD_SET(sockfd,&readfds);
int ret_select = select(sockfd+1,&readfds,NULL,NULL,NULL);
error_Handling("select",ret_select);
if(FD_ISSET(0,&readfds))
{
ssize_t ret_read = read(0,&buf_data,sizeof(buf_data));
error_Handling("read",ret_read);
ssize_t ret_write = write(sockfd,&buf_data,sizeof(buf_data));
error_Handling("write",ret_write);
printf("我:%s\n",buf_data);
bzero(&buf_data,sizeof(buf_data));
}
if(FD_ISSET(sockfd,&readfds))
{
ssize_t ret_read = read(sockfd,&buf_data,sizeof(buf_data));
error_Handling("read",ret_read);
printf("%s:%s\n",inet_ntoa(serverAddr.sin_addr),buf_data);
bzero(&buf_data,sizeof(buf_data));
}
}
close(sockfd);
return 0;
}
void error_Handling(char* func,int retval)
{
if(retval == -1)
{
perror(func);
exit(1);
}
}
六 套接字的缓冲区以及阻塞模式
参考socket套接字及缓冲区详解
1.缓冲区
- 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区 和 输出缓冲区
- write()/send()/send to() 函数并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
- read()/recv()/recefrom() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
- 每个套接字的I/O缓冲区单独存在。
- 即使一端关闭套接字,也会继续传送这端套接字输出缓冲区中遗留的数据。
- 如果一端关闭套接字,这端将丢失输入缓冲区中的数据。
- 默认情况下,套接字为阻塞模式
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
2.使用write/send发送数据
阻塞模式下:
- 首先会检查输出缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据;
- 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。直到所有数据被写入缓冲区 write()/send() 才能返回。
- 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
- send()函数默认情况下会使用Nagle算法。Nagle算法通过将未确认的数据存入缓冲区直到积攒到一定数量一起发送的方法,来降低主机发送零碎小数据包的数目。所以假设send()函数发送数据过快的话,该算法会将一些数据打包后统一发出去。通过setsockopt()的TCP_NODELAY选项来禁用Nagle算法。
非阻塞模式下:
- write/send不做等待立即返回;
- write()/send()函数的过程仅仅是将数据拷贝到协议栈的缓冲区而已,如果缓冲区可用空间不够,则尽可能拷贝,返回成功拷贝的大小;
- 如果缓存区可用空间为0,则返回-1,同时设置errno为EWOULDBLOCK。
3.使用read/recv读取数据
read/recv函数返回其实际copy的字节数,如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。 阻塞模式下:
- 首先会检查输入缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来;(如果数据正在从输入缓冲区拷贝到用户空间,read/recv也会被阻塞)
- 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到输入缓冲区满,协议栈不能再接收数据。
非阻塞模式下:
- write/send不做等待立即返回;
- 成功返回实际读到的字节数;
- 如果输入缓冲区中没有数据,返回错误EWOULDBLOCK。
七 总结套接字收发数据的过程
TCP发送数据的过程:首先,TCP是有链接的可靠传输协议,所谓可靠也就是说保证客户端发送的数据服务端都能够收到,并且是按序收到。
-
数据首先由应用程序缓冲区复制到发送端的输出缓冲区(位于内核),注意这个过程是用类似write功能的函数完成的。有的人通常看到write成功就以为数据发送到了对端主机,其实这是错误的,write成功仅仅表示数据成功的由应用进程缓冲区复制到了输出缓冲区。 -
然后内核协议栈将输出缓冲区中的数据发送到对端主机,注意这个过程不受应用程序控制,而是发送端内核协议栈完成,其中包括使用滑动窗口、拥塞控制等功能。 -
数据到达接收端主机的输入缓冲区,注意这个接收过程也不受应用程序控制,而是由接收端内核协议栈完成,其中包括发送ack确认等。 -
数据由套接字接收缓冲区复制到接收端应用程序缓冲区,注意这个过程是由类似read等函数来完成。
思考:如果TCP服务端一直sleep,客户端一直发送数据,会出现什么情况?
- 阻塞模式下:
如果服务端一直sleep不接收数据,而客户端一直write,也就是只能执行上述过程中的前三步,这样最终结果肯定是接收端的输入缓冲区和发送端的输出缓冲区都被填满,这样write就无法继续将数据从应用程序复制到发送端的输出缓冲区了,从而使进程进入睡眠。 - 非阻塞情况下:
服务端一直sleep,随着客户端write,接收端的输入缓冲区和发送端的输出缓冲区会被填满。当发送端的输出缓冲区的可用空间为0时,write立即返回-1,并将errno置为EWOULDBLOCK。
“华清远见” http://www.hqyj.com/学习更多编程知识
|