[英]How can I use the same UDP socket both for sending and receiving packets? What am I missing in this code?
我正在嘗試編寫一個應用程序,其中客戶端應將UDP數據包發送到服務器,然后服務器應通過無線接口全部回復給客戶端。 客戶端和服務器都在同一個二進制文件中實現,並且用戶可以使用適當的命令行參數來選擇一種模式。
我正在使用UDP,但在使客戶端與服務器通信時遇到問題。 首先,在兩種情況下,我都嘗試使用相同的UDP套接字來接收和發送數據包。 我以為這是有可能的,但是我開始有些懷疑。
然后,這是客戶端和服務器的相關代碼:
struct sockaddr_in inaddr;
fd=socket(AF_INET,SOCK_DGRAM,0);
if(fd==-1) {
perror("socket() error");
exit(EXIT_FAILURE);
}
// Prepare sockaddr_in structure
bzero(&inaddr,sizeof(inaddr));
inaddr.sin_family=AF_INET;
inaddr.sin_port=htons(opts.port); // opts.port is parsed from the command line
inaddr.sin_addr.s_addr=opts.destIPaddr.s_addr; // opts.destIPaddr is parsed from the command line and already in the correct format
// Bind to the wireless interface (devname is previusly obtained in a tested piece of code)
if(setsockopt(sData.descriptor,SOL_SOCKET,SO_BINDTODEVICE,devname,strlen(devname))==-1) {
perror("setsockopt() for SO_BINDTODEVICE error");
close(sData.descriptor);
exit(EXIT_FAILURE);
}
兩者都將使用以下方式進行讀寫:
sendto(fd,packet,packetsize,0,(struct sockaddr *)&(inaddr),sizeof(inaddr))
和:
struct sockaddr_in srcAddr;
socklen_t srcAddrLen=sizeof(srcAddr);
// .....
recvfrom(fd,packet,MAX_PACKET_SIZE,0,(struct sockaddr *)&srcAddr,&srcAddrLen);
問題是客戶端和服務器無法通信,並且對於每個發送的數據包,客戶端似乎總是收到“端口不可達”的ICMP數據包(在Wireshark中,我可以清楚地看到,客戶端發送了正確的UDP數據包,而服務器拒絕了它們, “無法訪問的端口”)。
可能我沒有以適當的方式使用UDP套接字:您知道我在這里缺少的內容嗎? 我的最終目標是:
devname
-但是也要獲取其IP地址或MAC地址應該不是問題) 盡管從問題本身還不清楚,但是您的注釋似乎表明您沒有像@Someprogrammerdude所推斷的那樣, bind()
指向套接字的地址。 在這種情況下,重要的是要理解bind()
作用與SO_BINDTODEVICE
套接字選項所服務的目的不同,並且在很大程度上是正交的,盡管在選項名稱中使用了“ BIND”。
bind()
函數用於將套接字與一個地址相關聯,該地址對於TCP和UDP包括一個端口號。 SO_BINDTODEVICE
關於將套接字限制為通過特定設備的數據。 盡管實際上,IP地址和網絡接口之間通常存在一對一的映射,
POSIX系統接口不是特定於IP協議套件的,因此要注意避免假定地址族都具有與IP相似的特征。
即使對於IP,一個網絡接口也可能具有多個地址。
特別是對於IP,無論如何,您都需要將套接字與端口關聯,然后系統才能接受該端口的入站流量。 套接字選項不會這樣做,甚至不會直接將套接字與IP地址相關聯。 那就是bind()
的作用。
在評論中你問
我是否總是需要使用不同的
struct sockaddr_in
,一個用於bind
,另一個用於sendto
? 我不能僅使用一種結構來獲得相同的結果嗎?
您可以重用套接字地址結構,但請注意, bind()
與sendto()
內容必須不同。 前者需要綁定到本地地址,而后者則需要將消息發送到的遠程地址。 我認為將單獨的對象用於這些不同的目的會更干凈一些,但這不是必需的。
至於讓客戶端選擇自己的端口,正如我在評論中所說,這是UDP的通常操作模式。 實際上,通常如此,如果您尚未綁定套接字,則在第一次調用sendto()
時, 系統會為您處理。 您已經在使用recvfrom()
,服務器(以及客戶端)也可以通過recvfrom()
獲取發送每條消息的對等方的地址。 您應該能夠將該地址對象反饋回sendto()
以發送響應。 因此,它應該像確保服務器bind()
以便偵聽眾所周知的端口號一樣容易,但是確保客戶端不這樣做,以便使用系統自動分配的端口號。
我個人將UDP服務器綁定到通配符地址和特定端口,並使用IP_PKTINFO
套接字選項獲取接口和目標地址,作為每個數據包的輔助消息。
本質上,啟用IP_PKTINFO
套接字選項意味着您使用recvmsg()
收到的每個數據包都會收到IPPROTO_IP
級別的IP_PKTINFO
類型的ancillary message
。
同樣,在發送響應時,可以在輔助IP_PKTINFO
消息中使用ipi_ifindex
或ipi_spec_dst
成員來告訴內核如何路由該消息。
這樣,您可以僅綁定到一個(或兩個,如果同時使用IPv4和IPv6)通配符套接字,並使用它通過所需的任何接口接收和發送UDP數據包。 特別是,使用相同的接口和源IP地址將客戶端用作目標。 每當有新接口可用時,您的服務器端都會立即對這些接口做出響應(盡管顯然可以根據來自它們的接口將不需要的客戶端請求丟棄在地板上)。 簡單,而且很健壯。
以下示例server.c可能更好地說明了這一點:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <net/if.h>
#include <netdb.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
if (!done)
done = signum;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
return sigaction(signum, &act, NULL);
}
static inline const char *ip4_address(const struct in_addr addr)
{
static char buffer[32];
char *p = buffer + sizeof buffer;
unsigned char octet[4];
/* in_addr is in network byte order. */
memcpy(octet, &addr, 4);
/* We build the string in reverse order. */
*(--p) = '\0';
do {
*(--p) = '0' + (octet[3] % 10);
octet[3] /= 10;
} while (octet[3]);
*(--p) = '.';
do {
*(--p) = '0' + (octet[2] % 10);
octet[2] /= 10;
} while (octet[2]);
*(--p) = '.';
do {
*(--p) = '0' + (octet[1] % 10);
octet[1] /= 10;
} while (octet[1]);
*(--p) = '.';
do {
*(--p) = '0' + (octet[0] % 10);
octet[0] /= 10;
} while (octet[0]);
return p;
}
int main(int argc, char *argv[])
{
int ip4fd, ip4port;
char dummy;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s UDP-PORT-NUMBER\n", argv[0]);
fprintf(stderr, "\n");
return EXIT_FAILURE;
}
if (sscanf(argv[1], " %d %c", &ip4port, &dummy) != 1 || ip4port < 1 || ip4port > 65535) {
fprintf(stderr, "%s: Invalid UDP port number.\n", argv[1]);
return EXIT_FAILURE;
}
if (install_done(SIGHUP) ||
install_done(SIGINT) ||
install_done(SIGTERM)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
ip4fd = socket(AF_INET, SOCK_DGRAM, 0);
if (ip4fd == -1) {
fprintf(stderr, "Cannot create an UDP socket: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
/* Set the IP_PKTINFO socket option, so each received datagram has an
ancillary message containing a struct in_pktinfo. */
{
int option = 1;
if (setsockopt(ip4fd, IPPROTO_IP, IP_PKTINFO, &option, sizeof option) == -1) {
fprintf(stderr, "Cannot set IP_PKTINFO socket option: %s.\n", strerror(errno));
close(ip4fd);
return EXIT_FAILURE;
}
}
/* Bind to the wildcard address, to receive packets using any network interface. */
{
struct sockaddr_in ip4addr;
ip4addr.sin_family = AF_INET;
ip4addr.sin_port = htons(ip4port);
ip4addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(ip4fd, (const struct sockaddr *)(&ip4addr), sizeof ip4addr) == -1) {
fprintf(stderr, "Cannot bind to port %d: %s.\n", ip4port, strerror(errno));
close(ip4fd);
return EXIT_FAILURE;
}
}
printf("Now listening on UDP port %d.\n", ip4port);
printf("Press CTRL+C, or send HUP, INT, or TERM (pid %ld) to exit.\n",
(long)getpid());
fflush(stdout);
/* Receive UDP messages, and describe them. */
{
unsigned char payload[4096], ancillary[1024];
char *iface, ifacebuf[IF_NAMESIZE + 1];
unsigned int iface_index;
struct in_addr iface_addr, dest_addr;
struct iovec iov;
struct msghdr hdr;
struct cmsghdr *cmsg;
struct sockaddr_in from;
struct in_pktinfo *info;
ssize_t len;
size_t i;
while (!done) {
iov.iov_base = payload;
iov.iov_len = sizeof payload;
hdr.msg_name = &from;
hdr.msg_namelen = sizeof from;
hdr.msg_iov = &iov;
hdr.msg_iovlen = 1;
hdr.msg_control = ancillary;
hdr.msg_controllen = sizeof ancillary;
hdr.msg_flags = 0;
/* Receive a new datagram. */
len = recvmsg(ip4fd, &hdr, 0);
if (len < 0) {
if (len == -1) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
continue;
fprintf(stderr, "Error receiving data: %s.\n", strerror(errno));
} else
fprintf(stderr, "recvmsg() error: Unexpected return value, %zd.\n", len);
close(ip4fd);
return EXIT_FAILURE;
}
/* Report. */
printf("Received %zu bytes from %s port %d:\n",
(size_t)len, ip4_address(from.sin_addr), ntohs(from.sin_port));
/* Check the ancillary data for the pktinfo structure. */
info = NULL;
for (cmsg = CMSG_FIRSTHDR(&hdr); cmsg != NULL; cmsg = CMSG_NXTHDR(&hdr, cmsg))
if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO)
info = (void *)CMSG_DATA(cmsg);
if (!info) {
fprintf(stderr, "Error: Packet is missing the IP_PKTINFO ancillary information!\n");
close(ip4fd);
exit(EXIT_FAILURE);
}
/* info may be unaligned. */
memcpy(&iface_index, &(info->ipi_ifindex), sizeof info->ipi_ifindex);
memcpy(&iface_addr, &(info->ipi_spec_dst), sizeof info->ipi_spec_dst);
memcpy(&dest_addr, &(info->ipi_addr), sizeof info->ipi_addr);
iface = if_indextoname(info->ipi_ifindex, ifacebuf);
/* Report the IP_PKTINFO information. */
if (iface)
printf(" Interface: %u (%s)\n", iface_index, iface);
else
printf(" Interface: %u\n", iface_index);
printf(" Local address: %s port %d\n", ip4_address(iface_addr), ip4port);
printf(" Real destination: %s port %d\n", ip4_address(dest_addr), ip4port);
for (i = 0; i < (size_t)len; i++) {
if (i == 0)
printf(" Data: 0x%02x", payload[i]);
else
if ((i & 15) == 0)
printf("\n 0x%02x", payload[i]);
else
printf(" 0x%02x", payload[i]);
}
if (len > 0)
printf("\n");
fflush(stdout);
/*
* Construct a response.
*/
payload[0] = 'O';
payload[1] = 'k';
payload[2] = '!';
payload[3] = '\n';
iov.iov_base = payload;
iov.iov_len = 4;
/* Keep hdr.msg_name and hdr.msg_namelen intact. */
hdr.msg_iov = &iov;
hdr.msg_iovlen = 1;
/* Prep the ancillary data. */
hdr.msg_control = ancillary;
hdr.msg_controllen = CMSG_SPACE(sizeof (struct in_pktinfo));
cmsg = CMSG_FIRSTHDR(&hdr);
cmsg->cmsg_level = IPPROTO_IP;
cmsg->cmsg_type = IP_PKTINFO;
cmsg->cmsg_len = CMSG_LEN(sizeof (struct in_pktinfo));
info = (void *)CMSG_DATA(cmsg);
/* info may be unaligned. */
memcpy(&(info->ipi_ifindex), &iface_index, sizeof info->ipi_ifindex);
memcpy(&(info->ipi_spec_dst), &iface_addr, sizeof info->ipi_spec_dst);
memcpy(&(info->ipi_addr), &from.sin_addr, sizeof info->ipi_addr);
hdr.msg_flags = 0;
/* Send the response. */
do {
len = sendmsg(ip4fd, &hdr, MSG_NOSIGNAL);
} while (len == -1 && errno == EINTR);
if (len == -1) {
fprintf(stderr, "Cannot send a response message: %s.\n", strerror(errno));
close(ip4fd);
return EXIT_FAILURE;
}
printf(" %zd-byte response sent successfully.\n", len);
fflush(stdout);
}
}
close(ip4fd);
return EXIT_SUCCESS;
}
使用gcc -Wall -O2 server.c -o server
編譯,然后運行以指定端口號作為命令行參數。 例如,./ ./server 4044
。
為了進行測試,我在客戶端使用了netcat:echo'Hello echo 'Hello!' | nc -q 1 -u theipaddress 4044
echo 'Hello!' | nc -q 1 -u theipaddress 4044
echo 'Hello!' | nc -q 1 -u theipaddress 4044
。
由於我在撰寫本文時已是星期五晚上,而且我懶於設置其他設備,因此我僅在一台機器上進行了非常輕松的測試。 邏輯是合理的; 只是我的實現可能會關閉。
如果您有任何疑問或看到錯誤或明顯錯誤,請在評論中告訴我,以便我進行驗證和解決。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.