嵌入式linux的网络编程(3)--TCP Client程序设计
CSDN2013年度博客之星评选活动开始,本人有幸入围参加评选,如果博客中的文章对你有所帮助,请为 ce123 投上宝贵一票,非常感谢!
投票地址:http://vote.blog.csdn.net/blogstaritem/blogstar2013/ce123
1.概述
客户端主要需要完成与服务器建立连接,请求数据,应答数据等工作.从代码上来看,客户端程序有很多代码与服务器端程序类似,我们先给出一个简单的客户端源码,随后进行详细的讲解.
1 /**************************************************************************************/
2 /*简介:TCPClient示例。 */
3 /*************************************************************************************/
4 #include <stdlib.h>
5 #include <stdio.h>
6 #include <errno.h>
7 #include <string.h>
8 #include <netdb.h>
9 #include <sys/types.h>
10 #include <netinet/in.h>
11 #include <sys/socket.h>
12
13 int main(int argc, char *argv[])
14 {
15 int sockfd;
16 char buffer[1024];
17 struct sockaddr_in server_addr;
18 struct hostent *host;
19 int portnumber,nbytes;
20
21 if(argc!=3)
22 {
23 printf("Usage:%s hostname portnumber\a\n",argv[0]);
24 exit(1);
25 }
26
27 if((host=gethostbyname(argv[1]))==NULL)
28 {
29 herror ("Get host name error\n");
30 exit(1);
31 }
32
33 if((portnumber=atoi(argv[2]))<0)
34 {
35 printf("Usage:%s hostname portnumber\a\n",argv[0]);
36 exit(1);
37 }
38
39 /* 客户程序开始建立 sockfd描述符 */
40 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
41 {
42 printf("Socket Error:%s\a\n",strerror(errno));
43 exit(1);
44 }
45 /* 客户程序填充服务端的资料 */
46 bzero(&server_addr,sizeof(server_addr));
47 server_addr.sin_family=AF_INET;
48 server_addr.sin_port=htons(portnumber);
49 server_addr.sin_addr=*((struct in_addr *)host->h_addr);
50 /* 客户程序发起连接请求 */
51 if(connect(sockfd,(struct sockaddr *)(&server_addr),\
52 sizeof(struct sockaddr))==-1)
53 {
54 printf("Connect Error:%s(%d)\a\n",strerror(errno),errno);
55 exit(1);
56 }
57 /* 连接成功了 */
58 if((nbytes=read(sockfd,buffer,1024))==-1)
59 {
60 printf("Read Error:%s\n",strerror(errno));
61 exit(1);
62 }
63 buffer[nbytes]='\0';
64 printf("I have received:%s\n",buffer);
65
66 /* 结束通讯 */
67 close(sockfd);
68 exit(0);
69 }
客户端程序首先连接到服务器端,然后才能进行数据交换,在这之前就必须知道服务器的IP地址和端口号.
2.名字地址转化
通常,人们在使用过程中都不愿意记忆冗长的IP 地址,尤其到IPv6时,地址长度多达128位,那时就更加不可能一次次记忆那么长的IP地址了.因此,使用主机名将会是很好的选择.在Linux中,同样有一些函数可以实现主机名和地址的转化,最为常见的有gethostbyname,gethostbyaddr,getaddrinfo等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化.其中gethostbyname是将主机名转化为IP地址,gethostbyaddr则是逆操作,是将IP地址转化为主机名,另外getaddrinfo还能实现自动识别IPv4地址和IPv6地址.
gethostbyname和gethostbyaddr都涉及到一个hostent的结构体,如下所示:
struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*地址类型*/
int h_length; /*地址长度*/
char **h_addr_list; /*指向IPv4或IPv6的地址指针数组*/
}
调用该函数后就能返回hostent结构体的相关信息.getaddrinfo函数涉及到一个addrinfo的结构体,如下所示:
struct addrinfo
{
int ai_flags; /*AI_PASSIVE,AI_CANONNAME;*/
int ai_family; /*地址族*/
int ai_socktype; /*socket类型*/
int ai_protocol; /*协议类型*/
size_t ai_addrlen; /*地址长度*/
char *ai_canoname; /*主机名*/
struct sockaddr *ai_addr;/*socket结构体*/
struct addrinfo *ai_next;/*下一个指针链表*/
}
对hostent结构体而言,addrinfo结构体包含更多的信息.gethostbyname函数语法要点如下:
如果该函数发生错误时,但全局变量errno中不存储错误代码,h_errno中存储的才是错误代码.通过herrno函数访问变量h_errno,该函数的使用与perror的用法一样,如果例子中的29行.
此外,还可以通过gethostname函数获得本地主机的名字,返回的名字可以用于gethostbyname,该函数的声明如下:
#include <unistd.h>
int gethostname(char *hostname, size_t size);
hostname: 一个指向将要存放主机名的缓冲区指针;size:缓冲区的长度.
getaddrinfo函数的语法要点如下:
在调用之前,首先要对hints服务线索进行设置.它是一个addrinfo 结构体,下图列举了该结构体常见的选项值.
3.连接服务器
当客户端完成创建socket,填充服务器信息结构等工作后,就可以连接服务器了,如例子中的第51行.connect函数的语法要点如下:
当connect函数出错时,全局变量errno含有下面的值:
EACCES,EPERM 用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败.
EADDRINUSE 本地地址处于使用状态.
EAFNOSUPPORT 参数serv_add中的地址非合法地址.
EAGAIN 没有足够空闲的本地端口.
EALREADY 套接字为非阻塞套接字,并且原来的连接请求还未完成.
EBADF 非法的文件描述符.
ECONNREFUSED 远程地址并没有处于监听状态.
EFAULT 指向套接字结构体的地址非法.
EINPROGRESS 套接字为非阻塞套接字,且连接请求没有立即完成.
EINTR 系统调用的执行由于捕获中断而中止.
EISCONN 已经连接到该套接字.
ENETUNREACH 网络不可到达.
ENOTSOCK 文件描述符不与套接字相关.
ETIMEDOUT 连接超时.
当connect函数成功返回后,就可以使用sockfd作为与服务器连接的套接字描述符,一些之前提到的I/O函数,都可以用来与服务器进行通信了.
4.测试结果
将这两个程序分别编译为simple_server和simple_client,通过SecureCRT建立两个和linux操作系统的连接,分别运行这两个程序.
注意:localhost是本地循环地址,它代表本机的IP地址,用点分十进制表示为127.0.0.1.这是一个特殊的IP地址,通常用来测试IP协议是否正常.在本地调试网络程序时会经常用到这个地址.