先看一个例子,下面是一个采用UDP协议的客户程序,调用sendto向服务器端发送数据,而服务器并不启动,sendto返回,表示套接字缓冲区中有可用空间,程序会阻塞在recvfrom。
net.h
#ifndef MY_NET_H
#define MY_NET_H
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>
#include <signal.h>
#define MAXLINE 4096
#define SA struct sockaddr
#define LISTENEQ 10
/*err_quit*/
void err_quit(const char* err_string)
{
printf("%s\n", err_string);
exit(-1);
}
/*err_sys*/
void err_sys(const char* err_string)
{
perror(err_string);
exit(-1);
}
/*Socket*/
int Socket(int domain, int type, int protocol)
{
int sockfd = socket(domain, type, protocol);
if (sockfd == -1)
err_sys("socket error");
return sockfd;
}
/*Inet_pton*/
int Inet_pton(int af, const char *src, void *dst)
{
int r;
if ((r = inet_pton(af, src, dst)) <= 0)
err_sys("inet_pton error");
return r;
}
/*Connect*/
int Connect(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen)
{
int r;
if ((r = connect(sockfd, addr, addrlen)) == -1)
err_sys("connect error");
return r;
}
/*Bind*/
int Bind(int sockfd,
const struct sockaddr *addr,
socklen_t addrlen)
{
int r;
r = bind(sockfd, addr, addrlen);
if (r == -1)
err_sys("bind error");
return r;
}
/*Listen*/
int Listen(int sockfd, int backlog)
{
int r;
r = listen(sockfd, backlog);
if (r == -1)
err_sys("listen error");
return r;
}
/*Accept*/
int Accept(int sockfd,
struct sockaddr *addr,
socklen_t *addrlen)
{
int r;
r = accept(sockfd, addr, addrlen);
if (r == -1)
err_sys("accept error");
return r;
}
/*Close*/
int Close(int fd)
{
int r = close(fd);
if (r == -1)
err_sys("close error");
return r;
}
/*Read*/
int Read(int fd, void *buf, size_t count)
{
int r;
r = read(fd, buf, count);
if (r == -1)
err_sys("read error");
return r;
}
/*Write*/
int Write(int fd, const void *buf, size_t count)
{
int r;
r = write(fd, buf, count);
if (r == -1)
err_sys("write error");
return r;
}
/*Writen*/
//返回剩余的字符
int Writen(int fd, const void *buf, int len)
{
int r;
const char* ptr;
int nleft = len;
if(len <= 0)
return -1;
ptr = buf;
while (1)
{
r = Write(fd, ptr, len);
nleft -= r;
ptr += r;
len -= r;
if (nleft == 0)
return 0;
}
}
/*Readn*/
int Readn(int fd, void *buf, int len)
{
int r;
char* ptr;
ptr = buf;
while (1)
{
r = Read(fd, ptr, len);
if (r == 0)
return 0;
if (len-r > 0)
{
len -= r;
ptr += r;
}
else if (len-r == 0)
return len;
}
}
#endif
客户程序
#include "net.h"
main()
{
int sockfd;
struct sockaddr_in addr;
char buf[1024];
int r;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(22222);
//addr.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
while(fgets(buf, 1024, stdin) != NULL)
{
sendto(sockfd, buf, strlen(buf), 0,
(struct sockaddr*)&addr, sizeof(addr));
r = recvfrom(sockfd, buf, 1024, 0, NULL, NULL);
if (r == -1)
{
perror("");
exit(-1);
}
buf[r] = 0;
printf("%s\n", buf);
}
}
用tcpdump工具查看,
发现第一行发送数据。
第二行服务器响应一个ICMP消息,提示端口不可达。这个ICMP错误是在sendto调用之后才得到,而这个错误不会返回给进程。
recvfrom可以返回的信息只有errno值,它没有办法返回出错数据报的目的ip和目的端口。既然端口不可达,程序又无法获得通知,如果UDP客户程序运行时,假如服务程序还没启动,那么客户程序如何才能知道服务器地址不可达呢?
一个基本规则是:对于一个UDP套接字,除非它已连接,否则由它引发的异步错误并不返回给它。
TCP套接字调用connect会引发三路握手,而UDP套接字则不会引发三路握手,只是检查是否存在错误,然后立即返回。
修改后的程序
#include "net.h"
main()
{
int sockfd;
struct sockaddr_in addr;
char buf[1024];
int r;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(22222);
//addr.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
Connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
while(fgets(buf, 1024, stdin) != NULL)
{
sendto(sockfd, buf, strlen(buf), 0,
(struct sockaddr*)&addr, sizeof(addr));
r = recvfrom(sockfd, buf, 1024, 0, NULL, NULL);
if (r == -1)
{
perror("");
exit(-1);
}
buf[r] = 0;
printf("%s\n", buf);
}
}
执行结果
启动客户进程后时并没有提示错误,因为connect不会引发三次握手,
输入后,将提示connection refused.
如果指定的ip地址并不存在,那么即使connect了,recvfrom也不会返回。
connect之后recvfrom返回使得程序得到通知的前提是该ip的主机存在,并且没有运行服务器程序。
调用connect并不给对端主机发送任何信息,只保存对端ip地址和端口号。在一个未绑定的套接字上调用connect也会给该套接字指派一个临时端口。
#include "net.h"
main()
{
int sockfd;
struct sockaddr_in addr1, addr2;
char buf[1024];
int r;
int len;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
addr1.sin_family = AF_INET;
addr1.sin_port = htons(22222);
inet_pton(AF_INET, "192.168.1.12", &addr1.sin_addr);
Connect(sockfd, (SA*)&addr1, sizeof(addr1));
Getsockname(sockfd, (SA*)&addr2, &len);
printf("ip:%s\n", inet_ntoa(addr2.sin_addr));
r = ntohs(addr2.sin_port);
printf("port:%d\n", r);
}
多次调用connect的情况:
1.为了指定新的对端ip和端口。
此时,connect函数的参数应为指向新指定对端的结构的指针,和结构的长度。
如下面的程序,先connect指定一个对端,之后再指定第二个对端,每次只可以与指定的对端之间通信。
客户
#include "net.h"
main()
{
int sockfd;
struct sockaddr_in addr1, addr2;
char buf[1024];
int r;
int len;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
addr1.sin_family = AF_INET;
addr1.sin_port = htons(22222);
inet_pton(AF_INET, "127.0.0.1", &addr1.sin_addr);
addr2.sin_family = AF_INET;
addr2.sin_port = htons(22223);
inet_pton(AF_INET, "127.0.0.1", &addr2.sin_addr);
Connect(sockfd, (SA*)&addr1, sizeof(addr1));
fgets(buf, sizeof(buf), stdin);
sendto(sockfd, buf, strlen(buf), 0, NULL, 0);
r = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
if (r == -1)
perror("");
Connect(sockfd, (SA*)&addr2, sizeof(addr2));//更改唯一对应的一个对端
fgets(buf, sizeof(buf), stdin);
sendto(sockfd, buf, strlen(buf), 0, NULL, 0);
r = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
if (r == -1)
perror("");
}
服务器
#include "net.h"
main()
{
int sockfd;
struct sockaddr_in addr;
char buf[1024];
int r;
int len;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(22223);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
Bind(sockfd, (SA*)&addr, sizeof(addr));
r = recvfrom(sockfd, buf, sizeof(buf),
0, (SA*)&addr, &len);
if (r == -1)
{
perror("recvfrom error");
exit(-1);
}
buf[r] = 0;
printf("%s\n", buf);
sendto(sockfd, buf, strlen(buf),
0, (SA*)&addr, len);
}
2.已连接的套接字,如果第二次connect前将套接字地址族设置为AF_UNSPEC,再调用connect,那么将断开之前的连接。也就是说不会返回错误,并且之后的发送数据要在函数里指定对端地址。
下面的例子,假如服务器没启动对应的服务程序,(服务器已启动)
#include "net.h"
main()
{
int sockfd;
struct sockaddr_in addr1, addr2;
char buf[1024];
int r;
int len;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
addr1.sin_family = AF_INET;
addr1.sin_port = htons(22222);
inet_pton(AF_INET, "127.0.0.1", &addr1.sin_addr);
Connect(sockfd, (SA*)&addr1, sizeof(addr1));
fgets(buf, sizeof(buf), stdin);
sendto(sockfd, buf, strlen(buf), 0, NULL, 0);
r = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
if (r == -1) //返回错误
perror("");
addr1.sin_family = AF_UNSPEC; //设置地址族
Connect(sockfd, (SA*)&addr1, sizeof(addr1));//在已连接的udp套接字上再次调用connect,将断开连接,之后recvfrom会阻塞,而不会返回错误
fgets(buf, sizeof(buf), stdin);
sendto(sockfd, buf, strlen(buf), 0, (SA*)&addr1, sizeof(addr1));
r = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
if (r == -1)
perror("");
}