send()、sendto()和recv()、recvfrom()的使用
udp通訊中的sendto()需要在引數裡指定接收方的地址/埠,recvfrom()則在引數中存放接收發送方的地址/埠,與之對應的send()和recv()則不需要如此,但是在呼叫send()之前,需要為套接字指定接收方的地址/埠(這樣該函式才知道要把資料發往哪裡),在呼叫recv()之前,可以為套接字指定傳送方的地址/埠,這樣該函式就只接收指定的傳送方的資料,當然若不指定也可,該函式就可以接收任意的地址的資料。(這些內容前面文章udp通訊中的connect()和bind()函式
有詳細講過)
這4個函式的使用比較簡單,但在一個例項中,遇到一個小問題。
實現功能: udp伺服器建立一個套接字接收客戶端的連線,連線成功後,伺服器再建立一個套接字與客戶端進行資料互動,要求儘量使用connect()和recv()、send()函式。
udp伺服器程式碼:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFSZ 1024
#define PORT 6567
int main(void)
{
int srv_sd, cli_sd;
int new_sd;
int ret;
struct sockaddr_in svr_addr, cli_addr;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buf[BUFSZ] = {};
//建立套接字
if ((srv_sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
//為伺服器套接字繫結埠
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(PORT);
svr_addr.sin_addr.s_addr = 0 ;
if ((ret = bind(srv_sd, (struct sockaddr* )&svr_addr, addrlen)) < 0)
{
perror("bind");
exit(EXIT_FAILURE);
}
//接收客戶端的連線
ret = recvfrom(srv_sd, buf, BUFSZ, 0, (struct sockaddr* )&cli_addr, &addrlen);
if (ret < 0)
{
perror("recvfrom");
exit(EXIT_FAILURE);
}
printf("server output msg:\n");
printf("client IPAddr = %s, Port = %d, buf = %s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
close(srv_sd);
//建立與客戶端資料互動的套接字
cli_sd = socket(AF_INET, SOCK_DGRAM, 0);
if (cli_sd < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
//為新套接字繫結地址資訊
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(6666);
svr_addr.sin_addr.s_addr = 0;
if ((ret = bind(cli_sd, (struct sockaddr* )&svr_addr, addrlen)) < 0)
{
perror("bind");
exit(EXIT_FAILURE);
}
//為新套接字指定目的地址,接下來的資料互動將可以採用recv()和send()
if ((ret = connect(cli_sd, (struct sockaddr* )&cli_addr, addrlen)) < 0)
{
perror("connect");
exit(EXIT_FAILURE);
}
//伺服器先發資料再收資料
while (1)
{
memset(buf, 0, BUFSZ);
printf("ple input: ");
fgets(buf, BUFSZ, stdin);
//sendto(sd, buf, BUFSZ, 0, (struct sockaddr* )&svr_addr, addrlen);
send(cli_sd, buf, BUFSZ, 0);
ret = recv(cli_sd, buf, BUFSZ, 0);
printf("server output msg:\n");
printf("client IPAddr = %s, Port = %d, buf = %s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
}
close(cli_sd);
return 0;
}
udp客戶端程式碼:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFSZ 1024
#define PORT 6567
int main(int argc, char *argv[])
{
int sd;
struct sockaddr_in svr_addr, cli_addr;
int ret;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buf[BUFSZ] = {};
//建立套接字
if ((sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
//為套接字繫結本地地址資訊
cli_addr.sin_family = AF_INET;
cli_addr.sin_port = htons(9693);
cli_addr.sin_addr.s_addr = 0;
if ((ret = bind(sd, (struct sockaddr* )&cli_addr, addrlen)) < 0)
{
perror("bind");
exit(EXIT_FAILURE);
}
//為套接字指定目的地址資訊,接下來的與伺服器的資料互動就可以使用
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(PORT);
svr_addr.sin_addr.s_addr = inet_addr("192.168.1.166");
if ((ret = connect(sd, (struct sockaddr* )&svr_addr, addrlen)) < 0)
{
perror("connect");
exit(EXIT_FAILURE);
}
//資料互動
while (1)
{
memset(buf, 0, BUFSZ);
printf("ple input: ");
fgets(buf, BUFSZ, stdin);
//sendto(sd, buf, BUFSZ, 0, (struct sockaddr* )&svr_addr, addrlen);
send(sd, buf, BUFSZ, 0);
memset(buf, 0, BUFSZ);
//ret = recvfrom(sd, buf, BUFSZ, 0, (struct sockaddr* )&svr_addr, &addrlen);
ret = recv(sd, buf, BUFSZ, 0);
printf("client output msg:\n");
printf("server IPAddr = %s, Port = %d, buf = %s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
}
close(sd);
return 0;
}
執行結果:
觀察程式流程,可以得出:
客戶端發起連線給伺服器,伺服器接收到後建立新的套接字並呼叫connect()函式為該套接字指定目標地址資訊,這個目標地址資訊雖然確實是客戶端的,但是客戶端的目標地址卻是伺服器,那麼伺服器新的套接字的目標地址不是客戶端而是伺服器,所以伺服器發出的資料還是自己收到。
程式的問題出現在客戶端,客戶端建立了套接字後,就立即為其制定目標(伺服器)的地址資訊,而這個目標地址資訊並非作為接下來資料互動的地址,所以應該把為客戶端指定目標地址操作放在伺服器建立新的sd之後返回資料到客戶端之後,但是注意,客服端建立完套接字後不能馬上為其connect()以指定目的地址資訊,那麼就發資料給伺服器時就要使用sendto()、接收資料則是用recvfrom(),流程圖改為:
客戶端程式碼實現為:
int main(int argc, char *argv[])
{
int sd;
struct sockaddr_in svr_addr, cli_addr;
int ret;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buf[BUFSZ] = {};
//建立套接字
if ((sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket");
exit(EXIT_FAILURE);
}
cli_addr.sin_family = AF_INET;
cli_addr.sin_port = htons(9693);
cli_addr.sin_addr.s_addr = 0;
if ((ret = bind(sd, (struct sockaddr* )&cli_addr, addrlen)) < 0)
{
perror("bind");
exit(EXIT_FAILURE);
}
//為sendto()函式的使用指定目標地址
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(PORT);
svr_addr.sin_addr.s_addr = inet_addr("192.168.1.166");
//傳送資料
memset(buf, 0, BUFSZ);
printf("ple input: ");
fgets(buf, BUFSZ, stdin);
sendto(sd, buf, BUFSZ, 0, (struct sockaddr* )&svr_addr, addrlen);
//接收資料,此時svr_addr的地址資訊是服務端新建的專為資料互動使用的sd
memset(buf, 0, BUFSZ);
ret = recvfrom(sd, buf, BUFSZ, 0, (struct sockaddr* )&svr_addr, &addrlen);
printf("client output msg:\n");
printf("server IPAddr = %s, Port = %d, buf = %s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
//為套接字繫結目標地址,目標地址是服務端專為資料互動使用的sd
if ((ret = connect(sd, (struct sockaddr* )&svr_addr, addrlen)) < 0)
{
perror("connect");
exit(EXIT_FAILURE);
}
//資料互動
while (1)
{
memset(buf, 0, BUFSZ);
printf("ple input: ");
fgets(buf, BUFSZ, stdin);
//sendto(sd, buf, BUFSZ, 0, (struct sockaddr* )&svr_addr, addrlen);
send(sd, buf, BUFSZ, 0);
memset(buf, 0, BUFSZ);
//ret = recvfrom(sd, buf, BUFSZ, 0, (struct sockaddr* )&svr_addr, &addrlen);
ret = recv(sd, buf, BUFSZ, 0);
printf("client output msg:\n");
printf("server IPAddr = %s, Port = %d, buf = %s\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
}
close(sd);
return 0;
}
執行結果:
結論: connect()和send()、recv()三個函式的搭配使用並不能說一定能代替sendto()、recvfrom(),具體使用還要依據程式碼場景。