嵌入式linux的网络编程(2)--TCP Server程序设计

嵌入式linux的网络编程(2)--TCP Server程序设计

CSDN2013年度博客之星评选活动开始,本人有幸入围参加评选,如果博客中的文章对你有所帮助,请为 ce123 投上宝贵一票,非常感谢!
投票地址:http://vote.blog.csdn.net/blogstaritem/blogstar2013/ce123

前面简单介绍了TCP/IP协议,事实上该协议非常复杂,要编写一个优秀的网络程序也非易事.下面我们通过一个例子的学习达到对网络编程有一个概貌性的理解.

1.TCP的通信过程

一个典型的TCP通信过程如下:


工作过程如下:服务器首先启动,通过调用socket建立一个套接字,然后调用bind将该套接字和本地网络地址联系在一起,再调用listen使套接字做好侦听的准备,并规定它的请求队列的长度,之后调用accept来接收连接.客户在建立套接字后就可以调用connect和服务器建立连接连接一旦建立,客户机和服务器之间就可以通过调用read和write来发送和接收数据.最后,待数据传送结束后,双方调用close关闭套接字.

2.TCP Server程序

为了学习基于socket编程的基本流程和所用到的API函数,下面我们通过一个实际的例子来学习.该例子包含服务器程序和客户端程序.首先列出服务器程序的源码,后续的文章中再讲解客户端程序.

  1 /**************************************************************************************/
  2 /*简介:TCPServer示例。                                                                  */
  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,new_fd;        
 16         struct sockaddr_in server_addr;  
 17         struct sockaddr_in client_addr;  
 18         int sin_size,portnumber;  
 19         const char hello[]="Hello\n";
 20 
 21         if(argc!=2) 
 22         { 
 23                 printf("Usage:%s portnumber\a\n",argv[0]); 
 24                 exit(1); 
 25         } 
 26         if((portnumber=atoi(argv[1]))<0) 
 27         { 
 28                 printf("Usage:%s portnumber\a\n",argv[0]); 
 29                 exit(1); 
 30         } 
 31 
 32         /* 服务器端开始建立socket描述符 */ 
 33         if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) 
 34         { 
 35                 printf("Socket error:%s\n\a",strerror(errno)); 
 36                 exit(1); 
 37         } 
 38 
 39         /* 服务器端填充 sockaddr结构 */ 
 40         bzero(&server_addr,sizeof(struct sockaddr_in)); 
 41         server_addr.sin_family=AF_INET; 
 42         server_addr.sin_addr.s_addr=htonl(INADDR_ANY); 
 43         server_addr.sin_port=htons(portnumber); 
 44 
 45         /* 捆绑sockfd描述符 */ 
 46         if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))== -1)
 47         {
 48                 printf("Bind error:%s\n\a",strerror(errno));
 49                 exit(1);
 50         }
 51 
 52         /* 监听sockfd描述符 */
 53         if(listen(sockfd,5)==-1)
 54         {
 55                 printf("Listen error:%s\n\a",strerror(errno));
 56                 exit(1);
 57         }
 58 
 59         while(1)
 60         {
 61                 /* 服务器阻塞,直到客户程序建立连接 */
 62                 sin_size=sizeof(struct sockaddr_in);
 63                 if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)
 64                 {
 65                         printf("Accept error:%s(%d)\n\a",strerror(errno),errno);
 66                         exit(1);
 67                 }
 68                 printf("Server get connection from %s\n",
 69                                 (char *)inet_ntoa(client_addr.sin_addr));
 70 
 71                 if(write(new_fd,hello,strlen(hello))==-1)
 72                 {
 73                         printf("Write Error:%s\n",strerror(errno));
 74                         exit(1);
 75                 }
 76                 /* 这个通讯已经结束 */
 77                 close(new_fd);
 78                 /* 循环下一个 */
 79         }
 80 
 81         close(sockfd);
 82         exit(0);
 83 }

接下来我们对该例子进行详细的讲解.

2.1.网络地址的表示

在引入的众多的头文件后,源码的16和17行定义了两个sockaddr_in数据类型的变量.它们分别表示服务器和客户端的网络地址.网络地址的表示主要通过socketaddr和sockaddr_in来表示.下面首先介绍两个重要的数据类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,如下所示:

struct sockaddr {
	unsigned short sa_family;	/*地址族*/
	char sa_data[14];		/*14字节的协议地址,包含该socket的IP地址和端口号。*/
};
struct sockaddr_in {
	short int sa_family; 		/*地址族*/
	unsigned short int sin_port;	/*端口号*/
	struct in_addr sin_addr;	/*IP地址*/
	unsigned char sin_zero[8];	/*填充0 以保持与struct sockaddr同样大小*/
};
// Internet address.
struct in_addr {
        union {
                struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { u_short s_w1,s_w2; } S_un_w;
                u_long S_addr; /* port in network byte order */
        } S_un;
#define s_addr  S_un.S_addr
};
// Socket address, internet style.
struct sockaddr_in {        // struct sockaddr的一种特殊形式
        short            sin_family;    /* address family: AF_INET */
        u_short        sin_port;        /* port in network byte order */
        struct in_addr sin_addr;        /* port in network byte order */
        char            sin_zero[8];    /* 8 byte pad */
};
// Structure used by kernel to store most addresses.
struct sockaddr {
        u_short sa_family; /* address family */
        char    sa_data[14]; /* up to 14 bytes of direct address */
};

这两个数据类型是等效的,可以相互转化,通常sockaddr_in数据类型使用更为方便.但需要注意的是sin_zero[8]是为了使两个结构体在内存中具有相同的尺寸,使用sockaddr_in的时候要把sin_zero全部设为0.在建立socketadd或sockaddr_in后,就可以对该socket进行适当的操作了.下图列出了该结构sa_family字段可选的常见值.


2.2.建立socket

第21行到30行是从命令行获得服务器的监听端口,第33行通过socket系统调用建立一个套接字.该系统调用的语法为:


返回-1时,errno值将设为下列这些值:

  • EPROTONOSUPPORT:错误原因是参数中的错误,表示申请的服务或指定的协议无效;
  • EMFILE:错误的原因是应用程序的描述符已满;
  • ENFILE:错误的原因是应用程序内部的系统文件表已满;
  • ENOBUF:错误的原因是系统没有可用的缓冲空间

2.3.绑定本地地址

我们先跳过40-43行,在46行有一个重要的函数bind.该函数帮助指定一个套接字使用的地址和端口.使用socket函数得到一个套接字描述符后,根据需要可能需要将socket绑定上一个本地的地址和端口,有以下两种情况:

  1. 当需要进行端口监听(listen)操作,等待接受一个连入请求时,一般都需要经过这一步.比如网页服务器等;
  2. 如果只是想进行连接一台已有的服务器,也就是进行connect操作的时候,这一步不是必须的.

bind系统调用的语法如下:


如果调用失败函数返回-1,并设置errno值为EADDRINUSER,最常见的错误时该端口已经被其他程序绑定.

40-43行的代码填充了一个struct sockaddr结构的变量.其中需要注意42和43行:

42   server_addr.sin_addr.s_addr=htonl(INADDR_ANY);  
43   server_addr.sin_port=htons(portnumber); 

server_addr.sin_addr.s_addr中存储的是需要绑定的IP地址,INADDR_ANY是一个宏定义,表示任意的IP地址.在服务器程序中表示接受所有的外部连接,如果需要指定某个具体的IP地址,可采用下面的方式:

server_addr.sin_addr.s_addr=inet_addr("192.168.1.100");

表示绑定的IP地址为192.168.1.100,server_addr.sin_port中存放的是需要绑定的端口号,可以为0~65535共65536个端口,有以下三点需要注意:

当指定端口号为0时,表示由系统动态分配一个可用的端口;

使用端口号小于1024的端口号需要root权限,一般设置为1024以上的端口号就能满足需要了;

如果设置的端口号已经分配给了别的进程,那么bind函数将出错,并设置errno为EADDRINUSER.

2.4.字节顺序转换

在42和43行中还有两个重要的函数要介绍:htonl和htos函数.它们的作用是进行网络字节顺序转换.由于每个机器内部对变量的字节存储顺序不同(有的系统高位在前,地位在后;有的系统地位在前,高位在后),而网络传输的数据是一定要统一顺序的.所有对于内部字节顺序和网络字节顺序不同的的机器,就一定要对数据进行转换(比如IP地址和端口).如果内部字节顺序和网络字节顺序相同,也要调用转换函数,但真正是否转换有系统函数自己来决定.

转换函数有四个:htons,ntohs,htonl,ntohl.这四个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s 代表short,l 代表long.通常16位的IP端口号用s代表,而IP地址用l来代表.下面给出它们的语法:


调用该函数只是使其得到相应的字节序,用户不需清楚该系统的主机字节序和网络字节序是否真正相等.如果是相同不需要转换的话,该系统的这些函数会定义成空宏.

在struct sockaddr_in中的sin_addr和sin_port它们的字节顺序都是网络字节顺序,而,sin_family确是主机字节序.这是为什呢?

sin_addr和sin_port是从IP和UDP,TCP协议中取出来的数据,而IP和UDP,TCP是直接和网络相关的,所以,它们必须使用网络字节顺序.然后sin_family只是内核用来判断struct sockaddr_in是存储的什么类型的数据,并且sin_family永远不会被发送到网络上,所以可以使用主机顺序来存储.

2.5.IP地址格式转换

为了绑定一个指定的IP地址,需要使用下面的函数:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef uint32_t in_addr_t;
in_addr_t inet_addr(const char *cp);
inet_addr函数能够把一个用数字和点表示的IP地址字符串转换成一个无符号长整型数.如下面的例子:

server_addr.sin_addr.s_addr=inet_addr("192.168.1.100");
经过该函数的转换后,不需要再使用htons或者htonl函数进行字节顺序转换,因为inet_addr返回的地址已经是网络字节序.

其实,这段代码有一个很不好的习惯,因为这段代码没有进行错误检查.如果inet_addr函数执行错误,它将返回-1.二进制的无符号整型值-1相当于一个广播用的IP地址(255,255,255,255),所以在一个健壮的网络程序中,应该对所有可能出现的问题的代码进行错误检查和处理.如果想把struct in_addr里面存的网络地址显示出来,可以使用inet_ntoa函数:

char *inet_ntoa(struct in_addr in);
inet_ntoa函数将一个32为的网络字节顺序的struct in_addr结构体转换成相应的点分十进制字符串(ntoa,表示Network to ASCII).inet_ntoa函数返回一个字符串指针,它指向一个定义在函数inet_ntoa中的static类型的字符串.所以,每次调用该函数,都会将这个static类型字符串改为最后一次调用inet_ntoa函数时得到的结果.如果需要将结果保存下来,可以每次调用后用memcpy将结果保存到另外一个自己的字符串中.

此外,inet_aton函数和inet_ntoa函数的功能相反,其定义如下:

int inet_aton(const char *cp, struct in_addr *inp);
如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零.使用这个函数并没有错误码存放在errno中,所以它的值会被忽略.

上面讲解的inet_aton,inet_addr和inet_ntoa是在IPv4 中用到的函数,而IPv4 和IPv6兼容的函数有inet_pton和inet_ntop.由于IPv6是下一代互联网的标准协议,因此这里也介绍一下这两个函数,inet_pton函数是将点分十进制地址映射为二进制地址,而inet_ntop是将二进制地址映射为点分十进制地址.

inet_pton函数的语法要点如下:


inet_ntop函数的语法要点如下:


2.6.listen函数

当调用bind函数,将一个套接字绑定到某个端口之后,就可以通过调用listen函数来准备接受客户端提出的连接请求.该函数的语法如下:


listen函数将一个套接字转换成一个倾听套接字,它主要做了下面两件事.

  • socket函数建立的套接字是一个未连接的套接字,这是还不能接受内核向此套接字提供的连接请求,调用listen函数之后就将这个套接字由CLOSE状态转为LISTEN状态,这时才可以准备接收内核发出的连接请求信号.
  • 由于可能会同时又很多连接请求需要处理,listen函数可以确定连接请求队列的长度,也就是backlog,本地能够等待的最大连接数目就是backlog的数值.

2.7.accept函数

第63行使用了accept函数,当服务器端执行了listen函数后,一般就使用accept函数来响应连接请求,建立连接并产生一个新的socket描述符来描述该连接.


accept函数默认为阻塞函数,调用该函数后,将一直阻塞直到有连接请求.如果执行成功,返回值是由内核生成的一个新的socket,同时将远程计算机的地址信息参数填充到参数addr所指的内存空间中.

在63行中,new_fd为accept函数返回后得到的新的socket,随后就可以通过这个socket实现服务器和客户端之间的数据通信.这时便有了两个套接字描述符:socket和new_fd,前者用于倾听客户端的连接请求,后者用于与已经连接的客户端进行数据通信.

一般一个服务器只需要生成一个倾听套接字且一直存在,这样在连接队列头部的连接请求就可以和这个套接字连接.当执行了accept函数后,内核为每一个新连接的客户端都重新生成了一个与客户端连接的套接字,这个套接字在完成服务器和客户端的通信之后就可以关闭了.而用于倾听的套接字则需要在退出服务器时才关闭.

在上面的例子中,通过一个while循环来重复地与客户端建立连接,倾听套接字socketfd在while循环开始之前便建立完毕,而与客户端通信的new_fd则是在该循环中建立的.在关闭的顺序上,采取在第77行关闭new_fd,注意这时还在while循环中,而直到第81行才关闭socketfd.

2.8数据通信

一旦成功建立了TCP连接,得到了一个socket,剩下要做的就是数据通信了.由于socket的本质就是文件描述符,因此,凡是基于文件描述符的I/O函数几乎都可以用于数据通信,如read,write,put,get等.上面的例子中就是使用write给客户端发送消息.

但是read或者write函数并不完全是针对套接字而设计的,虽然也能使用套接字接收或发送数据,但缺少对网络的控制选项.下面我们介绍专门用于套接字数据接收和发送的函数.

send函数的语法如下:


recv函数的语法如下:


send函数在调用后会返回它真正发送数据的长度.有一点需要引起注意的是:send函数所发送的数据可能少于它的参数所指定的长度.因为如果赋给send的参数包含的数据长度远大于send所能一次发送的数据,则send函数只发送它所能发送的最大数据长度.所以如果send函数的返回值小于len,那么需要再次发送剩下的数据,以保证数据的完整性.如果要发送的数据包足够小,那么send一般都会一次发送完毕的.

和很多函数一样,send函数如果发生错误,则返回-1,错误码存储在全局变量errno中.recv返回它所能真正收到的数据长度(也就是buf中的数据长度).如果返回-1则代表发送的错误(比如网络以外中断,对方关闭了套接字等),全局变量errno里面存放了错误代码.

下面两个函数用于无连接的UDP通信.sendto函数的语法如下:


recvfrom函数的语法如下:


2.9关闭连接

当程序进行网络传输完毕后,就应该关闭这个套接字描述符所表示的连接.实现这步非常简单,只需要使用标准的关闭文件的函数close即可.执行close后,套接字将不再允许进行读操作和写操作.任何有关对套接字描述符进行读操作和写操作都会收到一个错误.

如果需要对网络套接字的关闭进行进一步的操作,则可以使用shutdown.它允许进行单向的关闭操作,或是直接禁止网络通信.

#include<sys/socket.h>
int shutdown(int sockfd,int how);
  • 如果how参数为0,则该套接口上的后续接收操作将被禁止.这对于低层协议无影响.对于TCP协议,TCP窗口不改变并接收前来的数据(但不确认)直至窗口满.对于UDP协议,接收并排队前来的数据.任何情况下都不会产生ICMP错误包.
  • 若how为1,则禁止后续发送操作.对于TCP,将发送FIN.
  • 若how为2,则同时禁止收和发.
请注意shutdown()函数并不关闭套接口,且套接口所占有的资源将被一直保持到close socket()调用.

服务器程序的内容到这里就介绍完毕了,这个服务器的功能还是十分简陋的,同一时刻只允许有一个客户端与之连接,必须当该连接断开后才能与其他的客户端连接.这个问题通常可以采取创建子进程或子线程的方法来实现多个连接,在后面的文章中将给出一个采用多线程实现的服务器例程.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值