考虑以下情况:
此处「Thank you」的传递是多余的,这只是用来模拟客户端断开连接前还有数据要传输的情况。此时程序实现的难度并不小,因为传输文件的服务器端只需连续传输文件数据即可,而客户端无法知道需要接收数据到何时。客户端也没办法无休止的调用输入函数,因为这有可能导致程序阻塞。
这种方式也有问题,因为这意味这文件中不能有与约定字符相同的内容。为了解决该问题,服务端应最后向客户端传递 EOF 表示文件传输结束。客户端通过函数返回值接受 EOF ,这样可以避免与文件内容冲突。那么问题来了,服务端如何传递 EOF?
当然,调用 close 函数的同时关闭 I/O 流,这样也会向对方发送 EOF 。但此时无法再接受对方传输的数据。换言之,若调用 close 函数关闭流,就无法接受客户端最后发送的字符串「Thank you」。这时需要调用 shutdown 函数,只关闭服务器的输出流。这样既可以发送 EOF ,同时又保留了输入流。下面实现收发文件的服务器端/客户端。
#include
/* Shut down all or part of the connection open on socket FD.
HOW determines what to shut down:
SHUT_RD = No more receptions;
SHUT_WR = No more transmissions;
SHUT_RDWR = No more receptions or transmissions.
Returns 0 on success, -1 for errors. */
extern int shutdown (int __fd, int __how) __THROW;
//成功返回0,错误返回-1。
/* Send N bytes of BUF to socket FD. Returns the number sent or -1.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);
/* Read N bytes into BUF from socket FD.
Returns the number read or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);
flags参数为数据收发提供了额外的控制,它可以取表所示选项中的一个或几个的逻辑或。
/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
ADDR_LEN bytes long). Returns the number sent, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
int __flags, __CONST_SOCKADDR_ARG __addr,
socklen_t __addr_len);
/* Read N bytes into BUF through socket FD.
If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
the sender, and store the actual size of the address in *ADDR_LEN.
Returns the number of bytes read or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
int __flags, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);
/* Structure for scatter/gather I/O. */
struct iovec
{
void *iov_base; /* Pointer to data. */
size_t iov_len; /* Length of data. */
};
/* Structure describing messages sent by
`sendmsg' and received by `recvmsg'. */
struct msghdr
{
void *msg_name; /* Address to send to/receive from. */
socklen_t msg_namelen; /* Length of address data. */
struct iovec *msg_iov; /* Vector of data to send/receive into. */
size_t msg_iovlen; /* Number of elements in the vector. */
void *msg_control; /* Ancillary data (eg BSD filedesc passing). */
size_t msg_controllen; /* Ancillary data buffer length.
!! The type should be socklen_t but the
definition of the kernel is incompatible
with this. */
int msg_flags; /* Flags on received message. */
};
/* Receive a message as described by MESSAGE from socket FD.
Returns the number of bytes read or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern ssize_t recvmsg (int __fd, struct msghdr *__message, int __flags);
在实际应用中,我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是: 1O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这- -点可通过如下系统调用实现:
#ifdef __USE_XOPEN2K
/* Determine whether socket is at a out-of-band mark. */
extern int sockatmark (int __fd) __THROW;
#endif
sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark 返回I,此时我们就可以利用带MSG_0OB标志的reev调用来接收带外数据。如果不是,则sockatmark返回0。
/* Put the local address of FD into *ADDR and its length in *LEN. */
extern int getsockname (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __len) __THROW;
/* Put the address of the peer connected to socket FD into *ADDR
(which is *LEN bytes long), and its actual length into *LEN. */
extern int getpeername (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __len) __THROW;
/* Put the current value for socket FD's option OPTNAME at protocol level LEVEL
into OPTVAL (which is *OPTLEN bytes long), and set *OPTLEN to the value's
actual length. Returns 0 on success, -1 for errors. */
extern int getsockopt (int __fd, int __level, int __optname,
void *__restrict __optval,
socklen_t *__restrict __optlen) __THROW;
/* Set socket FD's option OPTNAME at protocol level LEVEL
to *OPTVAL (which is OPTLEN bytes long).
Returns 0 on success, -1 for errors. */
extern int setsockopt (int __fd, int __level, int __optname,
const void *__optval, socklen_t __optlen) __THROW;
//成功时返回0,失败时返回-1并设置error
sockfd参数指定被操作的目标socket。level 参数指定要操作哪个协议的选项(即属性),比如IPv4、IPv6、 TCP等。option_ name 参数则指定选项的名字。我们在表中列举了socket通信中几个比较常用的socket 选项。option_ value 和option_ len 参数分别是被操作选项的值和长度。不同的选项具有不同类型的值,如表中“数据类型”一列所示。
值得指出的是,对服务器而言,有部分socket选项只能在调用listen系统调用前针对业听socket设置才有效。这是因为连接socket只能由accept调用返回,而accept从listen听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列的连接至少已进入SYN_ RCVD状态,这说明服务器已经往接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项。对这种情况,Linux给开发人员提供的解决方案是:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括: SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、sO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。而对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。
IP地址比域名发生变更的概率要高,所以利用IP地址编写程序并非上策。
#include
/* Description of data base entry for a single host. */
struct hostent
{
char *h_name; /* Official name of host. */
char **h_aliases; /* Alias list. */
int h_addrtype; /* Host address type. */
int h_length; /* Length of address. */
char **h_addr_list; /* List of addresses from name server. */
#ifdef __USE_MISC
# define h_addr h_addr_list[0] /* Address, for backward compatibility.*/
#endif
};
/* Return entry from host data base for host with NAME.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern struct hostent *gethostbyname (const char *__name);
//成功时返回 hostent 结构体地址,失败时返回 NULL 指针。
调用 gethostbyname 函数后,返回的结构体变量如图:
/* Return entry from host data base which address match ADDR with
length LEN and type TYPE.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern struct hostent *gethostbyaddr (const void *__addr, __socklen_t __len,int __type);
//成功时返回 hostent 结构体地址,失败时返回 NULL 指针。
getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重人取决于其内部调用的gethostbyname和getservbyname函数是否是它们的可重人版本。该函数的定义如下:
/* Extension from POSIX.1:2001. */
#ifdef __USE_XOPEN2K
/* Structure to contain information about address of a service provider. */
struct addrinfo
{
int ai_flags; /* Input flags. */
int ai_family; /* Protocol family for socket. */
int ai_socktype; /* Socket type. */
int ai_protocol; /* Protocol for socket. */
socklen_t ai_addrlen; /* Length of socket address. */
struct sockaddr *ai_addr; /* Socket address for socket. */
char *ai_canonname; /* Canonical name for service location. */
struct addrinfo *ai_next; /* Pointer to next in list. */
};
/* Translate name of a service location and/or a service name to set of
socket addresses.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int getaddrinfo (const char *__restrict __name,
const char *__restrict __service,
const struct addrinfo *__restrict __req,
struct addrinfo **__restrict __pai);
hostname参数可以接收主机名,也可以接收字符串表示的IP地址(IPv4 采用点分十进制字符串,IPv6则采用十六进制字符串)。同样,service 参数可以接收服务名,也可以接收字符串表示的十进制端口号。hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。hints 参数可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果。result 参数指向一个链表,该链表用于存储getaddrinfo 反馈的结果。
addrinfo结构体中,ai_ protocol 成员是指具体的网络协议,其含义和socket系统调用的第三个参数相同,它通常被设置为0。ai_fags 成员可以取表中的标志的按位或。当我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。
getaddrinfo 将隐式地分配堆内存(可以通过valgrind等工具查看),因为res指针原本是没有指向一块合法内存的,所以,getaddrinfo 调用结束后,我们必须使用如下配对函数来释放这块内存。
/* Free `addrinfo' structure AI including associated storage. */
extern void freeaddrinfo (struct addrinfo *__ai) __THROW;
/* Translate a socket address to a location and service name.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int getnameinfo (const struct sockaddr *__restrict __sa,
socklen_t __salen, char *__restrict __host,
socklen_t __hostlen, char *__restrict __serv,
socklen_t __servlen, int __flags);
#endif /* POSIX */
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重人取决于其内部调用的gethostbyaddr和getservbyport 函数是否是它们的可重人版本。该函数的定义getnameinfo将返回的主机名存储在host参数指向的缓存中,将服务名存储在serv参数指向的缓存中,hostlen和servlen参数分别指定这两块缓存的长度。flags参数控制getnameinfo的行为,它可以接收表的选项。
getnameinfo和getaddrinfo函数成功时返回0,失败时返回错误码,可能的错误码如表:
Linux下strerror函数能将数值错误码error转换成易读的字符串形式,同样下面的函数可将表错误码转换成字符串形式。
/* Convert error return from getaddrinfo() to a string. */
extern const char *gai_strerror (int __ecode) __THROW;
#include
#include
#include
#include
#include
void error_handling(char *message);
int main(int argc, char *argv[])
{
int i;
struct hostent *host;
if (argc != 2)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
// 把参数传递给函数,返回结构体
host = gethostbyname(argv[1]);
if (!host)
error_handling("gethost... error");
// 输出官方域名
printf("Official name: %s \n", host->h_name);
// Aliases 解析的 cname 域名
for (i = 0; host->h_aliases[i]; i++)
printf("Aliases %d: %s \n", i + 1, host->h_aliases[i]);
//看看是不是ipv4
printf("Address type: %s \n",
(host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
// 输出ip地址信息
for (i = 0; host->h_addr_list[i]; i++)
printf("IP addr %d: %s \n", i + 1,
inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
#include
#include
#include
#include
#include
#include
void error_handling(char *message);
int main(int argc, char *argv[])
{
struct hostent *host;
struct sockaddr_in addr;
if (argc != 2)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
memset(&addr, 0, sizeof(addr));
addr.sin_addr.s_addr = inet_addr(argv[1]);
host = gethostbyaddr((char *)&addr.sin_addr, 4, AF_INET);
if (!host)
{
error_handling("gethost... error");
}
printf("Official name: %s \n", host->h_name);
for (int i = 0; host->h_aliases[i]; i++)
{
printf("Aliases %d:%s \n", i + 1, host->h_aliases[i]);
}
printf("Address type: %s \n",(host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
for (int i = 0; host->h_addr_list[i]; i++)
{
printf("IP addr %d: %s \n", i + 1,inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
}
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}