是否可能同时将bind()和connect()应用于两端,使它们只从彼此发送/接收数据? 这似乎是一个漂亮对称的方法。
你好,我来自2018年的遥远未来,向你们2012年问候。
实际上,connect()
一个UDP socket有其原因(尽管受到赐福的POSIX及其实现理论上不要求这样做)。
普通的UDP socket不知道它将要发送到哪个目标,所以每次调用sendmsg()
时都会执行一次路由查找。
然而,如果先调用connect()
并指定特定的远程接收方IP和端口,则操作系统内核将能够记下对路由的引用并将其分配给socket,从而使得在后续的sendmsg()
调用中,如果没有指定接收方 (否则之前的设置将被忽略),就能更快地发送消息并选择默认接收方。
if (connected)
rt = (struct rtable *)sk_dst_check(sk, 0);
if (!rt) {
[..skip..]
rt = ip_route_output_flow(net, fl4, sk);
[..skip..]
}
在Linux内核4.18之前,该功能主要局限于IPv4地址族。但是,自从4.18-rc4版本(并希望在Linux内核4.18正式版中)它也可以完全支持IPv6套接字。
这可能会带来显著的性能优势,尽管这将严重取决于您使用的操作系统。至少,如果您使用的是Linux,并且不使用套接字进行多个远程处理程序,那么应该尝试一下。
UDP是无连接的,因此对于操作系统实际上没有太多意义去建立某种连接。
在BSD套接字中,可以对UDP套接字执行connect
操作,但这基本上只是为send
设置了默认的目标地址(而不是明确地给send_to
)。
在UDP套接字上绑定(bind)指定了操作系统应该接受哪个本地接口地址的数据包(所有其他地址的数据包都将被丢弃),而与套接字类型无关。
当接收到数据包时,必须使用recvfrom
来识别数据包来自哪个源头。请注意,如果你想要一些身份验证方式,那么仅使用涉及的地址和端口就和没有任何锁一样不安全。TCP连接可以被劫持,而纯粹的UDP协议则很容易被IP欺骗攻击。你必须添加一些形式的HMAC认证。
bind()
的UDP套接字上使用recvfrom
,它仍将接受来自任意地址的数据包。 - datenwolfconnect
系统调用。bind
将一个套接字分配给本地地址。例如,如果您为同一服务(例如DNS)运行多个守护程序,则这很重要,为不同的地址提供服务。例如,手动的djbdns明确描述了如何在127.x.y.z:53上运行多个tinydns实例,在单个公共可达的dnscache后面进行代理。这个目的地的区别是使用bind
设置的! - datenwolfusage: ./<program_name> dst-hostname dst-udpport src-udpport
我测试了这段代码,开了两个终端。你应该能够向目标节点发送消息并从它那里接收消息。
在终端1中运行以下命令:
./<program_name> 127.0.0.1 5555 5556
在终端2中运行
./<program_name> 127.0.0.1 5556 5555
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define STDIN 0
int sendall(int s, char *buf, int *len)
{
int total = 0; // how many bytes we've sent
int bytesleft = *len; // how many we have left to send
int n;
while(total < *len) {
n = send(s, buf+total, bytesleft, 0);
fprintf(stdout,"Sendall: %s\n",buf+total);
if (n == -1) { break; }
total += n;
bytesleft -= n;
}
*len = total; // return number actually sent here
return n==-1?-1:0; // return -1 on failure, 0 on success
}
int main(int argc, char *argv[])
{
int sockfd;
struct addrinfo hints, *dstinfo = NULL, *srcinfo = NULL, *p = NULL;
int rv = -1, ret = -1, len = -1, numbytes = 0;
struct timeval tv;
char buffer[256] = {0};
fd_set readfds;
// don't care about writefds and exceptfds:
// select(STDIN+1, &readfds, NULL, NULL, &tv);
if (argc != 4) {
fprintf(stderr,"usage: %s dst-hostname dst-udpport src-udpport\n");
ret = -1;
goto LBL_RET;
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; //UDP communication
/*For destination address*/
if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo)) != 0) {
fprintf(stderr, "getaddrinfo for dest address: %s\n", gai_strerror(rv));
ret = 1;
goto LBL_RET;
}
// loop through all the results and make a socket
for(p = dstinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(p->ai_family, p->ai_socktype,
p->ai_protocol)) == -1) {
perror("socket");
continue;
}
/*Taking first entry from getaddrinfo*/
break;
}
/*Failed to get socket to all entries*/
if (p == NULL) {
fprintf(stderr, "%s: Failed to get socket\n");
ret = 2;
goto LBL_RET;
}
/*For source address*/
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; //UDP communication
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
/*For source address*/
if ((rv = getaddrinfo(NULL, argv[3], &hints, &srcinfo)) != 0) {
fprintf(stderr, "getaddrinfo for src address: %s\n", gai_strerror(rv));
ret = 3;
goto LBL_RET;
}
/*Bind this datagram socket to source address info */
if((rv = bind(sockfd, srcinfo->ai_addr, srcinfo->ai_addrlen)) != 0) {
fprintf(stderr, "bind: %s\n", gai_strerror(rv));
ret = 3;
goto LBL_RET;
}
/*Connect this datagram socket to destination address info */
if((rv= connect(sockfd, p->ai_addr, p->ai_addrlen)) != 0) {
fprintf(stderr, "connect: %s\n", gai_strerror(rv));
ret = 3;
goto LBL_RET;
}
while(1){
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
FD_SET(sockfd, &readfds);
/*Select timeout at 10s*/
tv.tv_sec = 10;
tv.tv_usec = 0;
select(sockfd + 1, &readfds, NULL, NULL, &tv);
/*Obey your user, take his inputs*/
if (FD_ISSET(STDIN, &readfds))
{
memset(buffer, 0, sizeof(buffer));
len = 0;
printf("A key was pressed!\n");
if(0 >= (len = read(STDIN, buffer, sizeof(buffer))))
{
perror("read STDIN");
ret = 4;
goto LBL_RET;
}
fprintf(stdout, ">>%s\n", buffer);
/*EOM\n implies user wants to exit*/
if(!strcmp(buffer,"EOM\n")){
printf("Received EOM closing\n");
break;
}
/*Sendall will use send to transfer to bound sockfd*/
if (sendall(sockfd, buffer, &len) == -1) {
perror("sendall");
fprintf(stderr,"%s: We only sent %d bytes because of the error!\n", argv[0], len);
ret = 5;
goto LBL_RET;
}
}
/*We've got something on our socket to read */
if(FD_ISSET(sockfd, &readfds))
{
memset(buffer, 0, sizeof(buffer));
printf("Received something!\n");
/*recv will use receive to connected sockfd */
numbytes = recv(sockfd, buffer, sizeof(buffer), 0);
if(0 == numbytes){
printf("Destination closed\n");
break;
}else if(-1 == numbytes){
/*Could be an ICMP error from remote end*/
perror("recv");
printf("Receive error check your firewall settings\n");
ret = 5;
goto LBL_RET;
}
fprintf(stdout, "<<Number of bytes %d Message: %s\n", numbytes, buffer);
}
/*Heartbeat*/
printf(".\n");
}
ret = 0;
LBL_RET:
if(dstinfo)
freeaddrinfo(dstinfo);
if(srcinfo)
freeaddrinfo(srcinfo);
close(sockfd);
return ret;
}
实际上,关键在于connect()
函数:
如果套接字sockfd的类型是SOCK_DGRAM,则addr是默认发送数据报的地址,也是唯一接收数据报的地址。
你的代码出现了问题:
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM; //UDP communication
/*For destination address*/
if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo))
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_flags = AI_PASSIVE;
相反,确保您检索到的addrinfo是您想要的。
换句话说,您创建的套接字可能不是UDP套接字,这就是它无法工作的原因。
本页面包含有关连接和未连接套接字的一些重要信息: http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html
以下引用回答了您的问题:
通常情况下,是UDP客户端调用connect,但也有一些应用程序中,UDP服务器与单个客户端长时间通信(例如TFTP);在这种情况下,客户端和服务器都可以调用connect。
我从未在UDP下使用过connect()。我觉得connect()在UDP和TCP下的设计目的完全不同。
man页面上有一些关于在UDP下使用connect()的简要说明:
通常情况下,基于连接的协议(例如TCP)套接字只能成功连接(connect())一次;而无连接的协议(例如UDP)套接字可以使用connect()多次来更改它们的关联性。
是的,你可以这样做。我也这么做。
而且你的使用情况正是这种方式有用的情况:双方都充当客户端和服务器,并且两侧只有一个进程。
我更多地从UDP提供的功能角度来看待它。UDP是一个8字节的头,其中添加了2字节的发送和接收端口(总共4字节)。这些端口与伯克利套接字交互,提供传统的套接字接口。也就是说,您不能在没有端口的情况下绑定地址,反之亦然。
通常情况下,当您发送UDP数据包时,接收端口(源)是临时的,而发送端口(目标)是远程计算机上的目标端口。您可以通过先绑定再连接来打败这种默认行为。现在,只要两台计算机上的相同端口空闲,您的源端口和目标端口将是相同的。
一般来说,这种行为(让我们称之为端口劫持)是不受欢迎的。这是因为您刚刚将发送端限制为仅能从一个进程发送,而不是在动态分配发送端源端口的临时模型中工作。
顺便说一句,UDP负载的另外四个字节,长度和CRC几乎完全无用,因为它们已经在IP数据包中提供,并且UDP头是固定长度的。电脑很擅长做一点减法,对吧?
void read_data(rio_request_t *req);
void read_data(rio_request_t *req) {
char *a = "CAUSE ERROR FREE INVALID";
if (strncmp( (char*)req->in_buff->start, "ERROR", 5) == 0) {
free(a);
}
// printf("%d, %.*s\n", i++, (int) (req->in_buff->end - req->in_buff->start), req->in_buff->start);
rio_write_output_buffer_l(req, req->in_buff->start, (req->in_buff->end - req->in_buff->start));
// printf("%d, %.*s\n", i++, (int) (req->out_buff->end - req->out_buff->start), req->out_buff->start);
}
int main(void) {
rio_instance_t * instance = rio_create_routing_instance(24, NULL, NULL);
rio_add_udp_fd(instance, 12345, read_data, 1024, NULL);
rio_add_tcp_fd(instance, 3232, read_data, 64, NULL);
rio_start(instance);
return 0;
}
connect()
只是为套接字设置默认的目标地址/端口。(你试过了吗?如果由于某些原因它不起作用,那就使用sendto()
。)个人而言,我会直接使用sendto()
,否则如果多个客户端连接到您的服务器,您会感到困惑。 - mpontillo