TCP和UDP在传输层区别
UDP是无连接不可靠的数据报协议。TCP提供面向连接的可靠字节流。
使用UDP常见应用
DNS(域名系统),NFS(网络文件系统),SNMP(简单网络管理协议)
(1)客户端不和服务器建立连接,只是使用sendto给服务器发送数据,必须指定服务器地址作为参数。
(2)服务器不接受来自客户端的连接,而只是使用recvfrom,等待来自客户端的数据达到。recvfrom同时返回
客户端的协议地址,用于服务器给客户端响应。
UDP Client UDP服务器
socket()
|
|
bind()众所周知端口
|
|
socket() recvfrom() <----------
| | |
| 数据请求 | |
--->sendto() -----------------> 阻塞,直到收到数据 |
| | | |
| | | |
| | 处理请求 |
| | | |
| | | |
| | 数据应答 | |
|---recvfrom() <----------------- sendto() ------------
|
|
|
close()
recvfrom函数:
#include
ssize_t recvfrom(int sockfd,void* buff,size_t nbytes,int flags,struct sockaddr* from,socklen_t *addrlen);
参数:sockfd : 套接字描述符
buff : 用于存放数据的缓冲区
nbytes : 缓冲区大小
flags : 暂时总设置为0
from : 用于存放UDP对端的套接字协议地址(输出参数)
addrlen : UDP对端的套接字协议地址字节大小(输出参数)
注意:最后两个参数from和addrlen可以得知该UDP数据是谁发送过来的。
如果设置from和addrlen为NULL,表示我们忽略对端信息。
返回值:
成功返回读取到的字节数,出错返回-1。返回值0是被允许的,不同于TCP中read返回0表示对端已经关闭。
sendto函数:
#include
ssize_t sendto(int sockfd,const void* buff,size_t nbytes,int flags,const struct sockaddr* to,socklen_t *addrlen);
参数:sockfd : 套接字描述符
buff : 要发送的数据内存
nbytes : 数据大小
flags : 暂时总设置为0
to : 指向接收者的套接字地址结构(输入参数)
addrlen : 上述套接字地址结构to的字节大小(输入参数)
注意:最后两个参数to和addrlen告知该数据要发给谁。
返回值:成功返回写入的字节数,出错返回-1
写入字节长度为0是被允许的,在UDP下,会形成一个只包含20字节的IP首部和一个8字节的UDP首部,没
有数据的IP数据报。
fgets sendto recvfrom
stdin----------> ----------------------------------->
UDP客户端 UDP服务器
stdout<--------- <----------------------------------
fputs recvfrom sendto
说明:
(1)通过socket指定参数SOCK_DGRAM创建UDP套接字。
(2)bind指定server端本地地址为INADDR_ANY(0),端口为众所周知。
(3)UDP中没有TCP中的EOF,因此recvfrom不会终止。
(4)recvfrom是阻塞的,因此提供的是一个迭代服务器,而不是并发服务器。
**//UDPserver代码**
#include
#include
#include
#include
#include
#include
using namespace std;
#define MAXLINE 1024
#define SERV_PORT 29988
void dg_echo(int sockfd,struct sockaddr* pcliaddr,socklen_t clilen){
int n;
socklen_t len;
char mesg[MAXLINE];
for(;;){
len = clilen;
n = recvfrom(sockfd,mesg,MAXLINE,0,pcliaddr,&len);
sendto(sockfd,mesg,n,0,pcliaddr,len);
}
}
int main(int argc , char** argv) {
int sockfd = -1;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
sockfd = socket(AF_INET , SOCK_DGRAM , 0);
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(0);
servaddr.sin_port = htons(SERV_PORT);
bind(sockfd , (struct sockaddr*)&servaddr , sizeof(servaddr));
dg_echo(sockfd , (struct sockaddr*)&cliaddr , sizeof(cliaddr));
return 0;
}
**//UDPClient代码**
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define MAXLINE 1024
#define SERV_PORT 29988
void dg_cli(FILE* fp,int sockfd,const SA* pservaddr,socklen_t servlen){
int n;
char sendline[MAXLINE];
char recvline[MAXLINE+1];
socklen_t len;
struct sockaddr* preply_addr;
preply_addr = (struct sockaddr*)malloc(servlen);
while(fgets(sendline,MAXLINE,fp)!=NULL){
sendto(sockfd,sendline,strlen(sendline),0,pservaddr,servlen);
len = servlen;
n = recvfrom(sockfd,recvline,MAXLINE,0,preply_addr,&len);
if(len!= servlen || memcmp(pservaddr,preply_addr,len)!=0){
continue;
}
recvline[n] = 0;
fputs(recvline,stdout);
}
}
int main(int argc , char** argv) {
int sockfd = -1;
struct sockaddr_in servaddr;
if(argc != 2){
cout << "usage:udpcli " << endl;
exit(-1);
}
sockfd = socket(AF_INET , SOCK_DGRAM , 0);
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET , argv[1],&servaddr.sin_addr);
dg_cli(stdin,sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
return 0;
}
注意:上述代码可能出现问题:
(1)如果客户端sendto的数据丢失(比如网络问题),则recvfrom一直阻塞。
(2)如果客户端sendto数据达到,但是服务器发出数据没有达到客户端,则客户端也将一直处于阻塞。
做如下测试:
(1)不启动UDPServer;
(2)开启tcpdump抓包:sudo tcpdump -i lo
(3)启动UDPClient:./UDPClient 127.0.0.1,发送数据。
结果:
此时并没有发生回射,在tcpdump我们发现如下信息:
15:41:39.535289 IP localhost.50872 > localhost.29988: UDP, length 7
15:41:39.535300 IP localhost > localhost: ICMP localhost udp port 29988
unreachable, length 43
第一条显示本地端口50872向本地端口29988服务器端口发送数据,长度为7字节UDP
第二条显示回了一条ICMP包: udp port 29988 unreachable,端口不可达。
总结:
服务并没有开启时会响应一个port unreachable的ICMP错误,但是该错误不会返回
给客户端,我们称这样错误为异步错误。
产生异步错误原因:
该异步错误由sendto引起,UDP输出操作sendto仅仅表示在接口输出队列中具有存放数据报的空间,并不
代表发送成功。该ICMP错误是之后真正发送时才产生。
解决异步错误的方法:
对于一个UDP套接字,由它引起的异步错误并不返回给它,除非它已经连接。在进程将其UDP套接字连接
到恰恰一个对端后,这些异步错误才返回给进程。
udp使用connect函数必要性:
解决udp无法接受异步错误的问题。
tcp和udp使用connect区别:
(1)没有tcp连接的三次握手协议
(2)内核只是检查是否存在一个立即可知的错误(例如上述的ICMP错误)
(3)内核记录对端的IP和port。
(4)立即返回。
未连接的UDP套接字:使用socket创建UDP套接字默认如此。
已连接的UDP套接字:调用connect结果。
udp套接字调用connect影响:
(1)发送数据时不能再指定IP和Port。即不能使用sendto,改用write和send
任何写入该套接字的数据会自动发往coonect指定对端。
(2)不必再使用recvfrom接收数据,改用read,recv,recvmsg。
(3)引发的任何异步错误,会立即返回给当前进程。
对一个udp多次调用connect:
(1)可以指定新的IP和Port
(2)把套接字地址结构地址族成员指定为AF_UNSPEC,则断开套接字。如果此时返回一个FAFNOSUPPORT
错误,可以忽略。
void dg_cli2(FILE* fp,int sockfd,const SA* pservaddr,socklen_t servlen){
int n;
char sendline[MAXLINE];
char recvline[MAXLINE+1];
connect(sockfd,(SA*)pservaddr,servlen);
while(fgets(sendline,MAXLINE,fp)!=NULL){
write(sockfd,sendline,strlen(sendline));
n = read(sockfd,recvline,MAXLINE);
if(n<0){
cout << strerror(errno) << endl;
return ;
}
recvline[n] = 0;
fputs(recvline,stdout);
}
}
调用connect指定对端IP和PORT;
使用read替换recvfrom,使用write替换sendto。
注意:如果服务器没有启动,在connect时并不会出错,当进行read时,则会返回错误:Connection refused。
如果使用的是TCP,则会进行三次握手,Connection refused则会在connect时就返回给进程。
测试实例:客户端不间断的向server发送大量数据,服务器不间断的接收数据。
//客户端发送数据
#define NDG 2000
#define DGLEN 1400
void dg_cli(FILE* fp,int sockfd,const SA* pservaddr,socklen_t servlen){
int i;
char sendline[DGLEN];
for(i=0;i0,pservaddr,servlen);
}
}
//服务器不间断接收数据并统计
static int count;
void dg_echo(int sockfd,SA* pcliaddr,socklen_t clilen) {
socklen_t len;
char mesg[MAXLINE];
signal(SIGINT,recvfrom_int);
for(;;){
len = clilen;
recvfrom(sockfd,mesg,MAXLINE,0,pcliaddr,&len);
count++;
}
}
static void recvfrom_int(int signo){
cout<<"received " << count << " datagrams" << endl;
exit(0);
}
使用命令netstat -s -udp 可以查看系统网络数据包信息,如下:
Udp:
245306 packets received
402051 packets to unknown port received.
3712 packet receive errors
651115 packets sent
RcvbufErrors: 3712
IgnoredMulti: 1
可以查看接收到包,没有接收的包,接收错误的包,以及发送的包。
测试结果:
Client不控制流量的发送,Server不间断的接收,有一定概率接收的包小于发送的包。
也就是说:有包丢失了。
结论:
发送或者接收UDP套接字有一定缓冲区,当缓冲区已经满时再发送数据,会导致数据的丢失。
同时一般发送端速度较快,接收端较慢,UDP发送端淹没接收端是轻而易举的。
设置UDP缓冲区大小:
int n = 220 * 1024;
setsockopt(sockfd,SOL_SOCKET,SO_RECBUF,&n,sizeof(n));