温故Linux后端编程(五):SOCKET网络编程
预备知识
- IP地址转换函数
- 在TCP/IP协议中,“IP地址+端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就称为端口号。
//旧版本就不看了,直接看新版本吧
#include<arpa/inet.h>
int inet_pton(int af,const char *src,void *dst); //[将“点分十进制” -> “整数”],这个函数转换字符串到网络地址.
//返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
//返回值:若成功则为指向结构的指针,若出错则为NULL
//支持ipv4和ipv6
//可重入函数
//参数释义
// af:参数既可以是AF_INET也可以是AF_INET6。
// src:第一个函数尝试转换由src指针所指向的字符串,并通过dst指针存放二进制结果
// inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。为有助于指定这个大小,在
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
//如果len太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- sockaddr 数据结构
struct sockaddr :很多网络编程函数的出现早于IPV4协议,为了向前兼容,现在sockaddr都退化成(void *)结构了。
传递一个地址给函数,然后由函数内部再强制类型转换为所需的地址类型。
struct sockaddr
{
unsigned short sa_family; /* address族, AF_xxx */
char sa_data[14]; /* 14 bytes的协议地址 */
};
// sa_family 一般来说, IPV4使用“AF_INET”。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
struct sockaddr_in
{
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};
//这两个数据类型是等效的,可以相互转换,通常使用sockaddr_in更为方便
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
补充1:
sockaddr和sockaddr_in的抉择:
前者是通用的套接字结构体,它可以在不同的协议族之间进行强转。
后者是以太网中采用的套接字结构体,因为前面那个不好用。
由于两个结构体的大小一致,所以进行地址结构设置的时候,通常的方法是使用后者进行配置,然后强制转换为前者的结构体类型,这样不会有任何副作用。
网络套接字函数
基于流套接字的网络编程流程:
//头文件
#include<sys/type.h>
#include<sys/socket.h>
- 1
- 2
- 3
- 4
- 5
- socket函数
//socket函数:
int socket(int domain,int type,int protocol);
//参数释义:
domain:
AF_INET:用来产生IPV4 - socket 的协议,使用TCP或UDP来传输,用IPV4的地址
AF_INET6:和上面的差不多,这个是IPV6的
AF_UNIX:本地协议,用在Unix和Linux系统上,一般都是服务端和客户端在同一台机器上时使用。
type:
SOCK_STREAM:这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,是用TCP协议来传输的。
SOCK_DGRAM:这个协议是无连接的,固定长度的连接调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET:这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。(注(1))必须把整个包完整的接收才能够进行读取。
SOCK_RAW:这个socket类型提供单一的网络访问
protocol:0,默认协议
返回值:
成功返回一个文件描述符,失败返回-1,设置errno。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
补充2:
以太网中建议使用PF_INET这个域。在程序设计时会发现有的代码使用的是AF_INET,在头文件中这两个值是一致的,我也经常写AF_INET,不过还是有细微的差别,PF_INET有些协议类型是AF_INET所没有实现的。
socket函数打开一个网络通讯端口,如果成功的话就像open一样返回一个文件描述符,应用程序可以像读写文件一样read/write在网络上收发数据。
- bind 函数
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
参数释义:
sockfd:socket文件描述符
addr:构造出IP地址加端口号
addrlen:sizeof(addr)的长度
- 1
- 2
- 3
- 4
- 5
- 6
客户端程序得之服务器程序的地址和端口号后就可以自动向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind函数的作用就是将参数sockfd和addr绑定在一起,使sock奋斗这个用于网络通信的描述符监听addr所描述的地址和端口号。
至于addr的配置可以参考前面的sockaddr自己配置,也可以看后面栗子里面的配置。
因为addr的配置不确定,所以需要第三个参数来指定结构体长度。
栗子:
struct sockaddr_in servaddr;
bzero(&servaddr,sizeof(servaddr)); //清空servaddr内容
servaddr.sin_family = PF_INET;
servaddr.sin_addr = htonl(INADDR_ANY); //这个宏表示任意的IP地址
//服务器一般有多个网卡,每个网卡也可能绑定了多个IP地址,这样可以设置在所有IP地址上监听,直到与某个客户端建立连接。
servaddr.sin_port = htons(8000);
- 1
- 2
- 3
- 4
- 5
- 6
- listen函数
int listen(int sockfd,int backlog);
参数释义:
backlog:排队建立三次握手队列和刚建立三次握手队列的链接数和
cat /proc/sys/net/ipv4/tcp_max_syn_backlog 查看系统默认backlog
listen函数声明sockfd处于监听状态,并且最多允许backlog个客户端处于连接状态,如果多了就忽略。
成功返回0,失败返回-1.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
补充3:
函数listen用来初始化服务器可连接队列。
服务器处理客户端连接时是顺序处理的,同一时间只能处理一个客户端连接。
当多个客户端的连接请求同时到来的时候,服务器将不能处理的客户端连接请求放入到等待队列中,这个队列的长度由listen()函数来指定。
大多数系统的设置为20,其实真的没必要太多,真的。
根据系统的可承受负载和程序的需求来确定。
系统有一个最大侦听队列数,一般是128(somaxconn),可以调优。
- accept函数
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
参数释义:
addr:传出参数,返回连接客户地址信息,含IP地址和端口号。
addrlen:传入addr的大小,返回真正的大小。
返回值:成功返回一个新的sockfd,用于和客户端通信,失败返回-1.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
三方握手完成后,服务器调用accept接收连接,如果服务器调用accept时还没有客户端请求连接,就阻塞等待直到有客户端连接上来。
如果addr传NULL,则表示不关心客户端的地址。
while(1)
{
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); //那个强转,历史遗留问题
n = read(connfd,buf,MAXLINE);
···
close(connfd);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- connect函数
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
参数释义:
addr:传入参数,指定服务器的地址信息,含IP地址和端口号。
返回值:成功返回0,失败返回-1.
- 1
- 2
- 3
- 4
- 5
- 6
客户端需要调用connect连接服务器
connect和bind形式一致,区别在于connect是用对方的地址。
- 补充5:
关闭套接字函数不止一个close,还有shutdown。
int shutdown(int sock,int how);
//该函数用于关闭双向连接的一部分。
/*
how:
SHUT_RD:值为0,表示切断读
SHUT_WR:值为1,表示切断写
SHUT_RDWR:值为2,和close功能相同
*/
CS模型 - TCP
- Server
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/type.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct saockaddr_in servaddr,cliaddr;
socklen_t cliaddr_len;
int listenfd,connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i,n;
// 套路开始
listenfd = socket(PF_INET,SOCK_STREAM,0); //创建一个网络套接字 bzero(&servaddr,sizeof(servaddr)); //清空结构体变量,准备开始刻画
servaddr.sin_family = PF_INET; //配置网络协议
servaddr.sin_port = htonl(SERV_PORT); //配置端口号
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //配置网络地址
bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); //好了,可以绑定了 别忘了历史遗留问题
listen(listenfd,20); //开始监听,允许20个进程进来
//开始接收数据了
printf("Accepting connections··· \n"); //写完一定要来检查一下这个换行,一不小心就忘记了
while(1)
{
cliaddr_len = sizeof(cliaddr); //这得实时更新
connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); //接收连接 n = read(connfd,buf,MAXLINE); //处理事务(这里为读取内容)
printf("Read from %s at port %d \n",inet_ntop(AF_INET,&cliaddr.sin_addr,str,sizeof(str)),ntohs(cliaddr.sin_port));
/*将客户端的地址读取到str里面然后打印*/ /*将端口号转换成整形数输出*/ for(i = 0;i < n; i++)
{ buf[i] = toupper(buf[i]); //换大写
} write(connfd,buf,n); //写回去
close(connfd);//用完关咯
}
//这里就不用return 0 了
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- Client
/*client.c*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/type.h>
#include<netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc,char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd,n;
char *str; if(argc!=2) //(注(2))
{
fputs("usage: ./c;ient message \n",stderr);
exit(1);
}
str = argv[1]; sockfd = socket(PF_INET,SOCK_STREAM,0); bzero(&servaddr,sizeof(servaddr)); //清空结构体变量,准备开始刻画 servaddr.sin_family = PF_INET;
inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT); bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));//请求连接服务器
write(sockfd,str,strlen(str)); //写入
n = read(sockfd,buf,MAXLINE);
printf("Response from server : \n···");
write(STDOUT_FIFENO,buf,n); //写入标准输出流
close(sockfd);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
setsockopt与getsockopt
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
- 1
- 2
- 3
- 4
- 5
- 6
参数释义:
sock:网络文件描述符
level:选项所在协议层。 如果想要在套接字层面上进行配置,则将此项设置为SOL_SOCKET。
optname:需要访问的选项名 (后面会有)(取决于level)
optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。
optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。
函数功能:
获取或设置与某个套接字关联的选项。
level指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项
- 1
- 2
- 3
- 4
以linux 2.6内核为例(在不同的平台上,这种关系可能会有不同),在套接字级别上(SOL_SOCKET),option_name可以有以下取 值:
SO_DEBUG,打开或关闭调试信息。
当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(第10)位,或清SOCK_DBG位。
SO_REUSEADDR,打开或关闭地址复用功能。
当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。
SO_DONTROUTE,打开或关闭路由查找功能。
当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。
SO_BROADCAST,允许或禁止发送广播数据。
当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。
SO_SNDBUF,设置发送缓冲区的大小。
发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。该操作将sock->sk->sk_sndbuf设置为val * 2,之所以要乘以2,是防止大数据量的发送,突然导致缓冲区溢出。最后,该操作完成后,因为对发送缓冲的大小 作了改变,要检查sleep队列,如果有进程正在等待写,将它们唤醒。
SO_RCVBUF,设置接收缓冲区的大小。
接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)和256字节。该操作将sock->sk->sk_rcvbuf设置为val * 2。
SO_KEEPALIVE,套接字保活。
如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时 器,否则关闭保活定时器。对于所有协议,该操作都会根据option_value置或清 sock->sk->sk_flag中的 SOCK_KEEPOPEN位。
SO_OOBINLINE,紧急数据放入普通数据流。
该操作根据option_value的值置或清sock->sk->sk_flag中的SOCK_URGINLINE位。
SO_NO_CHECK,打开或关闭校验和。
该操作根据option_value的值,设置sock->sk->sk_no_check。
SO_PRIORITY,设置在套接字发送的所有包的协议定义优先权。Linux通过这一值来排列网络队列。
这个值在0到6之间(包括0和6),由option_value指定。赋给sock->sk->sk_priority。
SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。
该选项的参数(option_value)是一个linger结构:
struct linger {
int l_onoff;
int l_linger;
};
如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
SO_PASSCRED,允许或禁止SCM_CREDENTIALS 控制消息的接收。
该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_PASSCRED位。
SO_TIMESTAMP,打开或关闭数据报中的时间戳接收。
该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_RCVTSTAMP位,如果打开,则还需设sock->sk->sk_flag中的SOCK_TIMESTAMP位,同时,将全局变量netstamp_needed加1。
SO_RCVLOWAT,设置接收数据前的缓冲区内的最小字节数。
在Linux中,缓冲区内的最小字节数是固定的,为1。即将sock->sk->sk_rcvlowat固定赋值为1。
SO_RCVTIMEO,设置接收超时时间。
该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。
SO_SNDTIMEO,设置发送超时时间。
该选项最终将发送超时时间赋给sock->sk->sk_sndtimeo。
SO_BINDTODEVICE,将套接字绑定到一个特定的设备上。
该选项最终将设备赋给sock->sk->sk_bound_dev_if。
SO_ATTACH_FILTER和SO_DETACH_FILTER。
关于数据包过滤,它们最终会影响sk->sk_filter。
以上所介绍的都是在SOL_SOCKET层的一些套接字选项,如果超出这个范围, 给出一些不在这一level的选项作为参数,最终会得到- ENOPROTOOPT的返回值。但以上的分析仅限
看着也很烦,看着也记不住。
再来些写好的栗子吧
1. closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:
BOOL bReuseaddr=TRUE;
setsockopt (s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));
2. 如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历
TIME_WAIT的过程:
BOOL bDontLinger = FALSE;
setsockopt (s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));
3. 在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:
int nNetTimeout=1000;//1秒
//发送时限
setsockopt (socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
//接收时限
setsockopt (socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
4. 在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节
(异步);系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据
和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:
// 接收缓冲区
int nRecvBuf=32*1024;//设置为32K
setsockopt (s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf=32*1024;//设置为32K
setsockopt (s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
5. 如果在发送数据的时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响
程序的性能:
int nZero=0;
setsockopt (socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));
6. 同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):
int nZero=0;
setsockopt (socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
7. 一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:
BOOL bBroadcast=TRUE;
setsockopt (s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));
8. 在client连接服务器过程中,如果处于非阻塞模式下的socket在connect()的过程中可
以设置connect()延时,直到accpet()被呼叫(本函数设置只有在非阻塞的过程中有显著的
作用,在阻塞的函数调用中作用不大)
BOOL bConditionalAccept=TRUE;
setsockopt (s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));
9 . 如果在发送数据的过程中(send()没有完成,还有数据没发送)而调用了closesocket(),以前我们
一般采取的措施是"从容关闭"shutdown(s,SD_BOTH),但是数据是肯定丢失了,如何设置让程序满足具体
应用的要求(即让没发完的数据发送出去后在关闭socket)?
struct linger {
u_short l_onoff;
u_short l_linger;
};
linger m_sLinger;
m_sLinger.l_onoff=1;//(在closesocket()调用,但是还有数据没发送完毕的时候容许逗留)
// 如果m_sLinger.l_onoff=0;则功能和2.)作用相同;
m_sLinger.l_linger=5;//(容许逗留的时间为5秒)
setsockopt (s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
recv&send函数
socket函数创建一个文件描述符fd,一个fd 对应两个缓冲区,一个输入缓冲区,一个输出缓冲区。
而recv和send函数就是对这两个函数进行操作。
recv函数
int recv(SOCKET s,char *buf, int len, int flags);
- 1
函数功能:不论客户端还是服务端都能通过recv从TCP另一端接收数据。
参数释义:
参数一:指定接收端套接字描述符;
参数二:指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
参数三:指明buf的长度;
参数四 :一般置为0。
send函数
int send( SOCKET s,char *buf,int len,int flags );
- 1
功能:不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
参数一:指定发送端套接字描述符;
参数二:存放应用程序要发送数据的缓冲区;
参数三:实际要发送的数据的字节数;
参数四:一般置为0。
运行过程
同步Socket的send函数的执行流程,当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的长度(因为待发送数据是要copy到套接字s的发送缓冲区的,注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里):
这时候就会出现以下情况:
1.如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;(切包准备去了解一下)
2.如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么 send就比较s的发送缓冲区的剩余空间和len:
(i)如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完; (ii)如果len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里。
- 1
- 2
- 3
3.如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
注意:send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)
同步Socket的recv函数的执行流程:当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,(发送先)
如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;
如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕;
当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数;
如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
我说明白了吗?
粘包
什么是粘包?
须知:只有TCP有粘包现象,UDP永远不会粘包
粘包不一定会发生
如果发生了:
1.可能是在客户端已经粘了
2.客户端没有粘,可能是在服务端粘了
我在其他地方看到两种解释:
应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
数据粘包问题的出现,是因为在客户端/服务器端都会有一个比较大的数据缓冲区,来存放接收的数据,为了保证能够完整的接收到数据,因此缓冲区都会设置的比较大。在收发数据频繁时,由于tcp传输消息的无边界,会导致客户端/服务器端不知道接收到的消息到底是第几条消息,因此,会导致类似一次性接收几条消息的情况,从而乱码。
总的来说,就是 客户端/服务器端 根本不知道你一串消息有多长,就像一个说话含糊不清的朋友跟你讲话,他如果不慢慢说,你就无法正确的断句。
粘包发生的两种情况:
1、发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小时,会当做一个包发出去,产生粘包)
2、接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
一种就是用特定的暗号截断,比方说:“完毕!”这种的,让人知道哪句是哪句。
另一种就是将消息的长度作为消息的一部分发送出去(包头)
代码可以参考这份博客:数据粘包处理
到这儿啦。
文章来源: lion-wu.blog.csdn.net,作者:看,未来,版权归原作者所有,如需转载,请联系作者。
原文链接:lion-wu.blog.csdn.net/article/details/113727582
- 点赞
- 收藏
- 关注作者
评论(0)