数据报socket
我们看到服务器端创建 UDP 套接字之后,绑定到本地端口,调用 recvfrom 函数等待客户端的报文发送;客户端创建套接字之后,调用 sendto 函数往目标地址和端口发送 UDP 报
文,然后客户端和服务器端进入互相应答过程。
交换数据报:recvfrom 和 sendto()
recvfrom()和 sendto()系统调用在一个数据报 socket 上接收和发送数据报
- flags是一个位掩码,它控制着了socket特定的I/O特性。如果无需使用其中任何一种特性,那么可以将flags指定为 0
- src_addr 和 addrlen 参数被用来获取或指定与之通信的对等 socket 的地址。
- 对于 recvfrom()来讲,src_addr 和 addrlen 参数会返回用来发送数据报的远程 socket 的地址。
- src_addr 参数是一个指针,它指向了一个与通信 domain 匹配的地址结构。
- addrlen 是一个值-结果参数。在调用之前应该将 addrlen 初始化为 src_addr 指向的结构的大小;在返回之后,它包含了实际写入这个结构的字节数。
- 如果不关心发送者的地址,那么可以将 src_addr 和 addrlen 都指定为 NULL。在这种情况下,recvfrom()等价于使用 recv()来接收一个数据报。也可以使用 read()来读取一个数据报,这等价于在使用 recv()时将 flags 参数指定为 0。
- 不管 length 的参数值是什么,recvfrom()只会从一个数据报 socket 中读取一条消息。如果消息的大小超过了 length 字节,那么消息会被静默地截断为 length 字节。
如果使用了recvmsg()系统调用,那么通过返回的msghdr结构中的msg_flags字段中的 MSG_TRUNC 标记来找出被截断的数据报,具体细节请参考 recvmsg(2)手册。
- 对于 sendto()来讲,dest_addr 和 addrlen 参数指定了数据报发送到的 socket。这些参数的使用方式与 connect()中相应参数的使用方式是一样的。dest_addr 参数是一个与通信 domain 匹配的地址结构,它会被初始化成目标 socket 的地址。addrlen 参数指定了 addr 的大小。
在 Linux 上可以使用 sendto()发送长度为 0 的数据报,但不是所有的 UNIX 实现都允许这样做的
NAME
recv, recvfrom - receive a message from a socket
SYNOPSIS
#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);
//UDP 报文每次接收都会获取对端的信息,也就是说报文和报文之间是没有上下文的。
UDP客户/服务器程序调用如下:
- 客户不与服务器建立连接,而是只管使用
sendto
函数给服务器发送数据报,其中必须指定目的地(服务器)的地址作为参数。 - 服务器不接受来自客户的连接,只管调用
recvfrom
函数,等待某个客户的数据到达,recvfrome将与所接受的数据报一道返回客户的协议地址,因此服务器可以把响应发送给正确的客户
ssize_t Recvfrom(int fd, void *ptr, size_t nbytes, int flags,
struct sockaddr *sa, socklen_t *salenptr)
{
ssize_t n;
if ( (n = recvfrom(fd, ptr, nbytes, flags, sa, salenptr)) < 0){
printf("recvfrom error");
exit(1);
}
return(n);
}
void Sendto(int fd, const void *ptr, size_t nbytes, int flags,
const struct sockaddr *sa, socklen_t salen)
{
if (sendto(fd, ptr, nbytes, flags, sa, salen) != (ssize_t)nbytes){
printf("sendto error");
exit(1);
}
}
在数据报 socket 上使用 connect()
尽管数据报socket是无连接的,但在数据报socket上应用connect()系统调用仍然是起作用的。在数据报socket上调用connect()会导致内核记录这个socket的对等socket的地址。术语已连接的数据报 socket 就是指此种 socket。术语非连接的数据报 socket 是指那些没有调用connect()的数据报 socket(即新数据报 socket 的默认行为)。
当一个数据报socket已连接之后:
- 可以在socket上使用write()或者send()来完成数据报的发送,并且会自动被发送到对等socket上。与sendto()一样,每个wirte()调用会发送一个独立的数据报
- 在这个socket上只能读取由对等socket发送的数据报
注意 connect()的作用对数据报 socket 是不对称的。上面的论断只适用于调用了 connect()数据报 socket,并不适用于它连接的远程 socket(除非对等应用程序在其 socket 上也调用了connect())。
为一个数据报 socket 设置一个对等 socket,这种做法的一个明显优势是在该 socket 上传输数据时可以使用更简单的 I/O 系统调用,即无需使用指定了 dest_addr 和 addrlen 参数的sendto(),而只需要使用 write()即可。
流socket VS 数据报socket
socket运行在同一主机或者通过网络连接起来的不同主机上的应用程序之间通信
- 一个socket存在于一个通信domain中,通信domain确定了通信范围和用来标识socket的地址格式。
- SUSv3 规定了 UNIX(AF_UNIX)、IPv4(AF_INET)以及 IPv6(AF_INET6)通信 domain。
大多数应用程序使用流 socket 和数据报 socket 中的一种。
- 流 socket(SOCK_STREAM)为两个端之间提供了一颗可靠的、双向的字节流通信信道。
- 数据报 socket(SOCK_DGRAM)提供了不可靠的、无连接的、面向消息的通信。
一个典型的流 socket 服务器
- 会使用 socket()创建其 socket,然后使用 bind()将这个 socket绑定到一个众所周知的地址上。
- 服务器接着调用 listen()以允许在该 socket 上接受连接。
- 监听socket 上的客户端连接是通过 accept()来接受的,它将返回一个与客户端的 socket 进行连接的新 socket 的文件描述符。
一个典型的流 socket 客户端
- 使用 socket()创建一个 socket,然后通过调用 connect()建立一个连接并制定服务器的众所周知的地址。
- 当两个流 socket 连接之后就可以使用 read()和 write()在任意一个方向上传输数据了。
- 一旦拥有引用一个流 socket 端点的文件描述符的所有进程都执行了一个隐式或显示的 close()之后,连接就会终止。
一个典型的数据报 socket 服务器
- 使用 socket()创建一个 socket,然后使用 bind()将其绑定到一个众所周知的地址上。
- 由于数据报 socket 是无连接的,因此服务器的 socket 可以用来接收任意客户端的数据报。
- 使用 read()或 socket 特定的 recvfrom()系统调用能够接收数据报,其中 recvfrom()能够返回发送 socket 的地址。
一个数据报 socket 客户端
- 使用 socket()创建一个 socket
- 然后使用 sendto()将一个数据报发送到指定的(即服务器的)地址上。
- connect()系统调用可以用来为数据报 socket 设定一个对等地址。在设定完对等地址之后就无需为发出去的数据报指定目标地址了;write()调用可以用来发送一个数据报。
UDP例子
UDP回射应用实现一
UDP回射服务器
#include<string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unp.h>
#define SERV_PORT 9877
#define MAXLINE 4096
#define SA struct sockaddr
void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr, cliaddr;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));
dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));
}
void dg_echo(int sockfd, SA *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);
}
}
- 这是一个迭代服务器。大多数TCP服务器是并发的,而UDP服务器是迭代的
- 对于UDP套接字,UDP层中隐藏有排队发生。事实上每个UDP套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字的接收缓冲区。当进程调用
recvfrom
时,缓冲区中的下一个数据报以fifo
顺序返回给套套接字,那么相继到达的数据报仅仅加到该套接字的接收缓冲区中。
UDP回射客户端
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2){
printf("usage: udpcli <IPaddress>");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));
exit(0);
}
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
while (fgets(sendline, MAXLINE, fp) != NULL) {
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}
- 对于一个UDP套接字,如果其进程在首次调用send’to的时候没有绑定一个本地端口,那么内核就会为它选择一个临时端口
- recvfrom的第五个和第6个参数是空指针,这告知内核我们并不关心应答数据报由谁发送
- UDP客户/服务端是不可靠的:
- 如果有一个客户数据报丢失(比如被客户主机与服务器主机之间的某个路由器丢弃),客户将永远阻塞在
dg_cli
函数中的Recvfrom
调用,等待一个永远不会到达的服务器应答。 - 如果客户数据报到达服务器,但是服务器的应答丢失了,客户将永远阻塞在
recvfrom
。 - 解决
recvfrom
的方法是给客户的recvfrom
调用设置一个超时。但是这不能真正解决问题
改进:
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
socklen_t len;
struct sockaddr *preply_addr;
preply_addr = 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) {
printf("reply from %s (ignored)\n",Sock_ntop(preply_addr, len));
continue;
}
recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}