注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

学习笔记

正确的方法如同学习书法,开始的时候要临摹,临摹好了然后创造自己的风格。

 
 
 

日志

 
 

[Linux笔记]Linux网络编程  

2011-10-14 19:06:19|  分类: Linux |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
1. TCP/IP协议概述 1.1 TCP/IP协议模型从一开始就遵循简单明确的设计思路,将TCP/IP的7层协议模型简化为4层——应用层、传输层、网络层、网络接口层。
*    网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接受。要注意的是数据帧是独立的网络信息传输单元。
*    网络层:负责将数据帧封装成IP数据包,并运行必要的路由算法。
*    传输层:负责端对端之间的通信会话连接与建立。传输协议的选择根据数据传输方式而定。
*    应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。

1.2 TCP/IP协议族
    虽然TCP/IP名称只包含了两个协议,但实际上,TCP/IP是一个庞大的协议族,它包括了各个层次上的众多协议,下图列举了各层中的一些重要的协议,并给出了各个协议在不同层次中所处的位置。
应用层:            telnet、ftp
传输层:            TCP、UDP
网络层:            ICMP、IGMP
                IPv4、IPv6
网络接口层:        MPLS
*    ARP:地址解析协议,实现通过IP地址得知物理地址。
*    MPLS:多协议标签协议,是很有发展前景的下一代网络协议。
*    IP:Internet Protocol,为计算机网络相互连接进行通信而设计的协议,负责在主机和网络之间寻址和路由数据包。
*    ICMP:Internet Control Message Protocol,用于在IP主机、路由器之间传递控制消息。控制消息指网络不通、主机是否可达、路由是否可用等网络本身的消息。
*    IGMP:Internet Group Management Protocol,用于IP主机向任一个直接相邻的路由器报告他们的组成员情况。IGMP信息封装在IP报文中,其IP的协议号为2。
*    TCP:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。
*    UDP:提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量的数据,可靠性由应用层来负责。

1.3 TCP和UDP
1.3.1 TCP
(1)概述
    同其他任何协议栈一样,TCP向相邻的高层提供服务。因为TCP的上一层就是应用层,因此,TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用TCP并使用TCP服务,提供需要准备发送的数据,用来区分接收数据应用的目的地址和端口号。
    通常应用程序通过打开一个socket来使用TCP服务,TCP管理到其他socket的数据传递。可以说,通过IP的源/目的可以唯一地区分网络中两个设备的关联,通过socket的源/目的可以唯一地区分网络中两个应用程序的关联。
(2)三次握手协议
    TCP对话是通过三次握手来初始化的。三次握手的目的是使数据段的发送和接受同步,告诉其他主机其一次可接受的数据量,并建立虚连接。
    下面描述了这三次握手的简单过程。
*    初始化主机通过一个同步标志置位的数据段发出回话请求。
*    接受主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发送的数据段的起始字节的顺序号、应答并带有将收到的下一个数据段的字节顺序号。
*      请求主机再回送一个数据段,并带有确认顺序号和确认号。
    TCP实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据包时,它将启动计时器。当数据包到达目的地后,接收方的TCP实体向回发送一个数据包,其中包含有一个确认序号,它意思是希望收到的下一个数据包的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据包。
(3)TCP数据包头
    TCP数据包头的含义为:
*      源端口、目的端口:16位长。标识出远端和本地的端口号。
*      顺序号:32位长。希望收到的下一个数据包的序列号。
*      确认号:32位长。希望收到的下一个数据包的序列号。
*      TCP头长:4位长。表明TCP头中包含多少个32位字。
*      6位未用。
*      URG:(Urgent Pointer field significant)紧急指针。用到的时候值为1,用来处理避免TCP数据流中断。
*      ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据包不包含确认信息,确认字段被忽略。
*      PSH:(Push Function),PUSH标志的数据,置1时请求的数据段在接收方得到后就可直接送到应用程序,而不必等到缓冲区满时才传送。
*      RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误。
*      SYN:(Synchronize sequence numbers)用来建立连接,在连接请求中,SYN=1,ACK=0,连接响应时,SYN=1,ACK=1。即,SYN和ACK来区分Connection Request和Connection Accepted。
*      FIN:(No more data from sender)用来释放连接,表明发送方已经没有数据发送了。
*      窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
*      校验和:16位长。是为了确保高可靠性而设置的。它校验头部、数据和伪TCP头部之和。
*      可选项:0个或多个32位字。包括最大TCP载荷,窗口比例、选择重发数据包等选项。

1.3.2 UDP
(1)概述
UDP即用户数据协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
UDP协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是在网络质量越来越高的今天,UDP的应用得到了大大的增强。它比TCP协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。
(2)UDP数据包头
    UDP数据包头的含义为:
*      源地址、目的地址:16位长。标识出远端和本地的端口号。
*      数据包的长度是指包括包头和数据部分在内的总的字节数。因为包头的长度是固定的,所以该域主要用来计算可变长度的数据部分。

1.3.3 协议的选择
协议的选择应该考虑到以下的3个方面。
(1)对数据可靠性的要求
    对数据要求高可靠性的应用需选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择UDP传送。
(2)应用的实时性
    由于TCP协议在传送过程中要进行三次握手、重传确认等手段来保证数据传输可靠性。使用TCP协议会有较大的时延,因此不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用。
(3)网络的可靠性
    由于TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,选择UDP协议来减少负荷。

2. 网络基础编程
2.1 socket概述
2.1.1 socket定义
    在Linux中的网络编程是通过socket接口来进行的。socket接口是一种特殊的I/O,它也是一种文件描述符。每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。
2.1.2 socket type类型
    常见的socket有3中类型:
(1)数据流socket(SOCK_STREAM)
    流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
(2)数据报socket (SOCK_DGRAM)
    数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。
(3)原始socket(SOCK_RAW)
    原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

2.1.3 协议簇protocol类型
(1)PF_UNIX,PF_LOCAL:用于本地进程间通信
(2)PF_INET:IPv4协议
(3)PF_INET6:IPv6协议
(4)PF_NETLINK:用于内核与应用程序通信
(5)PF_PACKET:用于访问底层数据
    常见的协议簇与socket类型组合及它们实际的作用:
协议簇                    socket类型                    作用
PF_UNIX,PF_LOCAL                SOCK_STREAM                    本地进程间数据流通信
                        SOCK_DGRAM                    本地进程间数据报通信
PF_INET                    SOCK_STREAM                    基于IPv4的数据流通信,使用TCP协议
                        SOCK_DGRAM                    基于IPv4的数据流通信,使用UDP协议
                        SOCK_RAW                    直接访问IPv4网络层数据
PF_INET6                    SOCK_STREAM                    基于IPv6的数据流通信,使用TCP协议
                        SOCK_DGRAM                    基于IPv6的数据流通信,使用UDP协议
                        SOCK_RAW                    直接访问IPv6网络层数据
PF_NETLINK                SOCK_DGRAM                    内核与应用程序间的通信
                        SOCK_RAW                    内核与应用程序间的通信
PF_PACKET                SOCK_DGRAM                    直接访问链路层原始数据,不包含链路层信息头
                        SOCK_RAW                    直接访问链路层原始数据,包含链路层信息头

2.2 地址及顺序处理
2.2.1 地址结构相关处理
    主要有两个重要的数据类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,定义在bits/socket.h中,如下所示:
#include <linux/socket.h>
struct sockaddr
{
       unsigned short sa_family;   /*地址族,AF_xxx*/
       char sa_data[14];    /*14字节的协议地址,包含该socket的IP地址和端口号*/
};
    其中,sa_family成员是地址类型,不同的协议族有不同的地址类型,对应于PF_INET协议族的地址类型为AF_INET。
    实际上,对于IPv4协议,我们经常使用另外一个数据类型表示地址,其定义如下:
#include <netinet/in.h>
typedef unsigned short  sa_family_t;
struct sockaddr_in
{
       sa_family_t sin_family; /*地址族*/
       unsigned short int sin_port;  /*端口号,例如:htons(3490)*/
       struct in_addr sin_addr;    /*IP地址*/
       unsigned char sin_zero[8];  /*填充0以保持与struct sockaddr同样大小*/
};
/* Internet address. */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};
    可以看出,整个由一个16位的端口号和一个32位的IP地址组成,这个地址可以被强制转换成(struct sockaddr)使用。这两个数据类型是等效的,可以相互转化,通常struct sockadd_in数据类型使用更为方便。在建立sockaddr或sockaddr_in后,就可以对该socket进行适当的操作了。

2.2.2 数据存储优先顺序
(1)函数说明
    计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了四个函数:htons、ntohs、htonl、ntohl。这四个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。
(2)函数格式说明
    这4个函数的语法格式是:
#include <arpa/inet.h>
uint16_t htons(uint16_t host16bit)
uint32_t htonl(uint32_t host32bit)
uint16_t ntohs(uint16_t net16bit)
uint32_t ntohl(uint32_t net32bit)
host16bit:主机字节序的16bit数据
host32bit:主机字节序的32bit数据
net16bit:网络字节序的16bit数据
net32bit:网络字节序的32bit数据
成功:返回要转换的字节序;出错:-1
    注意:调用上面的函数只是使其得到相应的字节序,用户不需要清楚该系统的主机字节序和网络字节序是否真正相等。如果是相同不需要转换的话,该系统的这些函数会定义成空宏。
2.2.3 地址格式转化
(1)函数说明
    通常用户在表达IP地址时采用的是以点分开的十进制字符串形式表示(或者是以冒号分开的十进制IPv6地址),每个被分隔的部分是一个取值不超过255的整数,而在socket编程时,使用的通常是32位二进制整数,这就需要将这两个数值进行转换。这里在IPv4中用到的函数有inet_aton、inet_addr和inet_ntoa,而IPv4和IPv6兼容的函有inet_pton和inet_ntop。由于IPv6是下一代互联网的标准协议,通常使用能够同时兼容IPv4和IPv6的函数。
    这里的inet_pton函数时将点分字符串转化为32位二进制整数地址,而inet_ntop是将32位二进制整数地址转化为点分字符串地址。
(2)函数格式
inet_aton和inet_ntoa函数
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr *inp);
    inet_aton函数可以将网络主机地址cp,从IPv4的十进制加小数点分隔的字符串转换为二进制(网络字节序),并存入inp指向的变量中,转换成功返回非0值,失败返回0。
    inet_ntoa函数可以将参数in所表示的以网络字节序排列的网络主机地址转化为IPv4的十进制加小数点分隔的字符串,并返回其指针。返回的字符串存储在静态分配的缓冲区中,它的后续调用将会覆盖。
    inet_pton函数的语法要点
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
    This function converts the character string src into a network address structure in the af address family, then copies the network address structure to dst.
af:    AF_INET IPv4协议
    AF_INET6 IPv6协议
src:要转换的字符串
dst:转化后的地址
成功:返回正值;失败:a)af不包含一个合法的地址族,返回负值,并设置errno为EAFNOSUPPORT,b)src不能转换为指定地址族中的有效网络地址。
    inet_pton函数的语法要点
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src , char *dst , socklen_t cnt);
    This function converts the network address structure src in the af address family into a character string, which is copied to a character buffer dst, which is cnt bytes long.
af:    AF_INET       IPv4协议
    AF_INET6      IPv6协议
src:要转换的地址
dst:转化后的字符串
cnt:字符串的长度
成功:返回一个指向dst的非空指针;失败:返回NULL,并设置erron。
2.2.4 名字地址转化
(1)函数说明
    通常,在使用过程中,人们都不愿意记忆冗长的IP地址,尤其是IPv6,地址长度多达128位。因此,使用主机名将会是一个很好的选择。在Linux中,同样有一些函数可以实现主机名和地址的转化,最为常见的有gethostbyname、gethostbyaddr、getaddrinfo等,它们都可以实现IPv4和IPv6的地址到主机名之间的转化。其中gethostbyname是将主机名转化为IP地址,gethostbyaddr则是逆操作,是将IP地址转化为主机名,另外,getaddrinfo还能实现自动识别IPv4地址和IPv6地址。
gethostbyname和gethostbyaddr都涉及到一个hostent的结构体,如下所示:
#include <netdb.h>
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.  */
  #define h_addr h_addr_list[0];
};
    调用该函数后就能返回hostent结构体的相关信息。
#include <netdb.h>
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.  */
};
(2)函数格式
gethostbyname函数的语法要点如下:
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname)
hostname:主机名
成功:hostent类型指针;出错:NULL,并在h_errno中设置错误码。
    调用该函数时可以首先对addrinfo结构体中的h_addrtype和h_length进行设置,若为IPv4可设置为AF_INET和4;若为IPv6可设置为AF_INET6和16;若不设置则默认为IPv4地址类型。     getaddrinfo函数语法要点如下:
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
int getaddrinfo(const char *node, const char *service , const struct addrinfo *hints , struct addrinfo **res);
hostname:主机名
service:设置网络中的每个套接字结构的端口号。
hints:指定首选的套接字类型或协议,可以为NULL。
res:指向一个动态分配的addrinfo结构的链表,由成员ai_next进行链接。
成功:返回0;失败:返回非零错误码。
    在调用之前,首先对hints进行设置,它是一个addrinfo结构体,下面列出了该结构体常见的选项值。
    addrinfo结构体常见的选项值
#include <netdb.h>
ai_flags      AI_PASSIVE:socket结构体中的网络地址不指定。
            AI_CANONNAME :通知getaddrinfo函数返回主机的名字
ai_family     AF_INET:IPv4协议
            AF_INET6:IPv6协议
            AF_UNSPE:IPv4或IPv6均可
ai_socktype  SOCK_STREAM:字节流套接字socket(TCP)
            SOCK_DGRAM:数据报套接字socket(UDP)
ai_protocol   IPPROTO_IP:IP协议
            IPPROTO_IPV4:IPv4协议
            IPPROTO_IPV6:IPv6协议
            IPPROTO_UCP:UDP
            IPPROTO_TCP:TCP
    注意:a) 通常服务器端在调用getaddrinfo之前,ai_flags设置AI_PASSIVE,用于bind函数(用于端口和地址的绑定),主机名nodename通常会设置为NULL。b) 客户端getaddrinfo时,ai_flags一般不设置AI_PASSIVE,但是主机名nodename和服务名servname(端口)则不应该为空。c) 即使不设置ai_flags为AI_PASSIVE,取出的地址也并非不可以被bind,很多程序中ai_flags直接设置为0,即3个标志位都不设置,这种情况下只要hostname和servname设置的没有问题就可以正确bind。
    设置IP地址常用的方法:
*    inet_aton(argv[1],&addr.sin_addr);
*    host=gethostname(argv[1]);
    addr.sin_addr=*((struct in_addr *)host->h_addr);
(3)使用实例
/*getaddrinfo.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    struct addrinfo hints , *res = NULL;
    int rc;
    memset(&hints,0,sizeof(hints));
    /*设置addrinfo结构体中的个参数*/
    hints.ai_family=PF_UNSPEC;
    hints.ai_socktype=SOCK_DGRAM;
    hints.ai_protocol=IPPROTO_UDP;
    /*调用getaddrinfo函数*/
    rc=getaddrinfo("127.0.0.1","123",&hints,&res);
    if(rc!=0)
    {
        perror("getaddrinfo!");
        exit(1);
    }
    else
        printf("getaddrinfo success!\n");
}
运行结果:
$ gcc getaddrinfo.c -o getaddrinfo
$ ./getaddrinfo
getaddrinfo success!

2.3 socket基础编程
2.3.1 函数说明
    进行socket编程的基本函数有socket、bind、listen、accept、send、sendto、recv、recvfrom这几个,其中对于客户端和服务器端以及TCP和UDP的操作流程都有所区别,这里先对每个函数进行一定的说明,再给出不同情况下使用的流程图。
*       socket:该函数用于建立一个socket连接,可指定socket类型等信息。在建立了socket连接之后,可对sockaddr或sockaddr_in进行初始化,以保存所建立的socket信息。
*       bind:用socket()函数只是创建了一个socket描述符,它存在于名字空间并没有指定地址,该函数用于将端口、IP地址等struct sockaddr信息绑定到socket描述符上,另外,它主要用于TCP的连接,而在UDP的连接中并不必要。
*        listen:使一个绑定地址的socket进入被动状态,用来接收来自其他主机的连接请求。
*        accept:已经启动侦听的socket可以接受连接请求,accept操作类似于读数据,有阻塞方式和非阻塞方式的区别,如果sockfd以阻塞方式操作,那么当它的连接请求队列为空的时候,对accept函数的调用会阻塞当前进程的执行;如果socket以非阻塞方式操作,那么当它的连接请求队列为空的时候,对accept函数的调用会失败,并设置errno变量。该操作会返回一个新的socket描述符,这是与对端连接成功的一个socket,可以通过它与对端进行通信,通信的方式是全双工,即可接受又可发送。原来进行侦听的socket仍然存在且状态不变,只是队列中少了一个连接请求。
*        connect:该函数在TCP中是用于bind之后的client端,用于发起并与服务器端建立连接,而在UDP中由于没有了bind函数,因此用connect有点类似bind函数的作用。
*       send和recv:这两个函数用于接收和发送数据,可以用在TCP中,也可以用在UDP中。当用在UDP中时,可以在connect函数建立连接之后再用。
*       sendto和recvfrom:这两个函数的作用与send和recv函数类似,也可以用在TCP和UDP中。当用在TCP时,后面的几个与地址有关的参数不起作用,函数作用等同于send和recv;当用在UDP时,可以用在之前没有使用connect的情况时,这两个函数可以自动寻找指定地址并进行连接。
    服务器端和客户端使用TCP协议的通讯模型如图1所示:
[Linux笔记]Linux网络编程 - Fantity Wei - 学习笔记
图1 使用TCP协议的socket通讯模型
    上述模型的步骤如下:
Sever端
1)    用socket()函数创建一个socket描述符。
2)    填充struct sockaddr,设置服务器使用的协议、端口号和可接受的IP范围等信息。
3)    用bind()函数struct sockaddr信息绑定到socket描述符上。
4)    用listen()函数设置允许的最大连接数。
5)    用accept()函数阻塞等待来自客户段的连接请求。
6)    用send()和recv()或者write()和read()函数来进行收发数据。
7)    关闭网络连接。
Client端
1)    用socket()函数创建一个socket描述符。
2)    填充struct sockaddr,设置服务器使用的协议、端口号和IP等信息。
3)    用connect()函数连接服务器。
4)    用send()和recv()或者write()和read()函数来进行收发数据。
5)    关闭网络连接。
    服务器端和客户端使用UDP协议的流程图如图2所示:
 
[Linux笔记]Linux网络编程 - Fantity Wei - 学习笔记
图2 使用UDP协议的socket通讯模型
上述模型的步骤如下:
Sever端
1)    用socket()函数创建一个socket描述符。
2)    填充struct sockaddr,设置服务器使用的协议、端口号和可接受的IP范围等信息。
3)    用bind()函数struct sockaddr信息绑定到socket描述符上。
4) 用recvfrom()函数循环接受
5)    关闭网络连接。
Client端
1)    用socket()函数创建一个socket描述符。
2)    填充struct sockaddr,设置服务器使用的协议、端口号和IP等信息。
3)    用bind()函数struct sockaddr信息绑定到socket描述符上。该操作可选。
4) 填充struct sockaddr,设置服务器使用的协议、端口号和IP等信息。
5) 用sendto()函数来发送数据。
6)    关闭网络连接。

2.3.2 函数格式
    socket函数的语法要点:
#include <sys/socket.h>
int socket(int socket_family, int socket_type, int protocol);
family:PF_INET:IPv4协议
       PAF_INET6:IPv6协议
       PF_LOCAL&AF_UNIX:UNIX域协议
       PF_NETLINK:用于内核与应用程序通信
       PF_PACKET:用于访问底层数据
socket_type:SOCK_STREAM:字节流套接字socket
            SOCK_DGRAM:数据报套接字socket
            SOCK_RAW:原始套接字socket
protocol:0——指定一个特定的协议族中的socket类型
成功:非负套接字描述符;失败:-1,并设置errno。
    bind函数的语法要点:
#include <sys/socket.h>
#include <sys/types.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
         Traditionally, this is called "assigning a name to a socket." When a socket is created with socket, it exists in a name space (address family) but has no name assigned.
socdfd:被绑定套接字描述符
my_addr:所绑定的socket地址
addrlen:地址长度
成功:0;出错:-1,并设置errno。
例如:
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(struct sockaddr_in));
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//可与任何网络通信
server_addr.sin_port=htons(portnumber);
result = bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr));
    端口号和地址在my_addr中给出了,若不指定地址,则内核随意分配一个临时端口给该应用程序。
    linsten函数语法要点:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
socket:进入侦听的socket描述符
backlog:请求队列中允许的最大请求数。
成功:0;出错:-1,并设置errno。
    accept函数语法要点:
#include <sys/socket.h>
#include <sys/types.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:要接受连接请求(服务器)的socket描述符
addr:用于保存对端(客户端)的socket地址
addrlen:地址长度
成功:返回与对端连接的新socket描述符;出错:-1,并设置errno。
    connect函数语法要点:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
sockfd:发起连接(客户端)的socket描述符
serv_addr:对端(服务器端)socket地址(服务器地址和端口)
addrlen:地址长度
    send函数语法要点:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int s, const void *buf, size_t len, int flags);
send a message on a socket. The system calls send(), sendto(), and sendmsg() are used to transmit a message to another socket. 只在socket处于稳定状态下时使用。
s:要发送数据的socket描述符
buf:指向要发送的数据的缓冲区
len:数据长度
flags:一般为0
成功:发送的字节;出错:-1,并设置errno。
    sendto函数语法要点:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
s:要接收的socket描述符
buf:指向要发送的数据的缓冲区
len:数据长度
flags:一般为0
to:目地机的IP地址和端口信息
tolen:地址长度
成功:发送的字节数;出错:-1,并设置errno。
    recv函数语法要点:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int s, const void *buf, size_t len, int flags); receive a message from a socket.
s:要接收数据的socket描述符
buf:存放接受数据的缓冲区
len:数据长度
flags:一般为0
成功:接收的字节;出错:-1,并设置errno。
    recvfrom函数语法要点:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
s:要发送数据的socket描述符
buf:指向要发送的数据的缓冲区
len:数据长度
flags:一般为0
to:目地机的IP地址和端口信息
tolen:地址长度
成功:接收的字节数;出错:-1,并设置errno。

2.3.3 使用socket选项
    socket有很多选项可以控制它的行为,这些选项需要用两个专门的函数来操作。
    获取socket选项的函数原型为:
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
sockfd:待操作的socket描述符
level:选项所在网络层次,在socket API层时,应设置为SOL_SOCKET;操作在其他层时,应是网络协议的编号,例如表明该选项由TCP来解释时,应设置为IPPROTO_TCP
optname:由整数代表的选项名字,是一个整数,都有一个方便记忆的宏定义
optval:指向存放选项值的内存,其值根据选项名称的不同而不同,因此用一个(void *)型指针代表
optlen:指向表示选项值长度的变量,调用前应将其赋值为所允许的最大长度(optval指向的缓冲区的长度),函数返回后为选项值的实际值。
成功:返回0,失败返回-1。
    设置socket选项的函数原型为:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    与getsockopt函数相比,首先它的optval参数变为只读指针型,因为函数不会对缓冲区中的数据进行修改,它指向的数据是要设置的选项值;其次optlen参数不再是指针型,它本身就表示选项值的长度,其余参数和返回值的含义都相同。

2.3.3.1 设置地址可重用
    socket设置了地址可重用之后,它所绑定的socket地址就可以同时被其他socket绑定。但是需要注意的是,同时绑定在一个地址上的socket仍然只有一个能进行侦听。
设置地址可重用的基本方法如下:
val = 1;
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,(void *)&val,sizeof(val));
    这是一个开关型的选项,将选项值设为0即可关闭地址可重用。
    处于安全方面的考虑,被socket绑定的地址在这个socket被关闭一段时间内仍然保持被绑定的状态,也就是不能为其他socket所绑定。这将对服务器程序的重新启动造成影响。设置地址可重用后,服务器程序就可以立刻重启,不必等待上次退出前绑定的地址重新成为可用状态。

2.3.3.2 设置缓冲区的大小
    每个socket在系统的底层都有发送和接受缓冲区,调整这些缓冲区的大小将影响通信的性能。设置发送缓冲区大小的基本方法如下:
int val = 1024;
setsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,(void *)&val,sizeof(val));
    设置接受缓冲区的基本方法如下:
int val = 128;
setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,(void *)&val,sizeof(val));

2.3.3.3 设置超时
    设置超时的意义在于,当以阻塞方式发送或接受数据时,进程的等待时间有一个上限。如果超过这个时间,则不管是否读到或写入数据,函数都会返回,这样就避免了进程一直阻塞在某个操作上。超时选项有两个,分别对应发送与接受操作,设置的基本方法如下:
struct timeval tv;
tv.tv_sec = timeout; /*超时的秒数*/
tv.tv_usec = 0; /*超时的微秒数*/
setsockopt(sockfd,SOL_SOCKET,SO_SNDTIMEO,(void *)&val,sizeof(val));
setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(void *)&val,sizeof(val));
    表示时间长度的数据型定义如下:
struct timeval
{
long tv_sec; /*秒*/
long tv_usec; /*微妙*/
}
    对于发送和接收操作,如果已经收到部分数据,则超时后仍然返回收到的字节数;如果超时后还没有收到数据,则返回-1,并且变量errno的值被设为EAGAIN,类似于非阻塞操作那样。如果超时设置为0,则相当与取消超时。connect操作受发送超时的影响。

2.3.4 阻塞与非阻塞操作
    默认情况下socket是以阻塞方式进行操作的。如果要改为非阻塞操作,则有两种方法可以采用。
    一种是利用发送和接受操作本身的参数,如:
send(sockfd,buf,len,MSG_DONTWAIT);
    其中,MSG_DONTWAIT使得本次操作成为非阻塞的。
    另一种方法是利用socket本身也是文件的特性,修改文件的标志位。与打开文件的open函数不同,打开socket的socket函数本身没有设置标志位的参数,需要在打开之后对标志位进行修改,可以使用fcntl函数实现。这是一个专门用来对文件描述符进行各种查询和设置操作的函数,其接口头文件及原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd ,long arg);
int fcntl(int fd, int cmd, struct flock *lock );
    其中参数fd是要操作的文件描述符,参数cmd代表要进行的操作,参数arg的含义与所要进行的操作有关,参数lock是进行文件锁相关操作时需的一个参数,函数的返回值与所要进行的操作有关。修改的方法如下:
int flag = fcntl(sockfd, F_GETFL); /*得到旧的标志位*/
flag |= O_NONBLOCK; /*增加非阻塞标志位*/
fcntl(sockfd,F_SETFL,flag); /*设置新的标志位*/
    设置了非阻塞标志之后,后续的相关操作都默认是非阻塞的。
send(sockfd,buf,len,MSG_NOSIGNAL);
    但即使这样,其他信号仍然会被发送,并且导致系统调用提前返回。如果要可靠地发送指定数量的字节,必须循环进行发送,如:
int send_byte(int sock,const char *buf,int len)
{
    int rc;
    int byte;
    for(byte = 0; byte < len; byte+=rc)
    {  
        rc = send(sock,buf+byte,len-byte,MSG_NOSIGNAL);
        if(rc < 0 && errno != EINTR)
        {  
            byte = -1;
            break;
        }  
    }  
    return byte;
}
    这个函数可以使用socket描述符sock可靠地发送len个字节,buf参数指向存放带发送数据的缓冲区。而可靠地接受指定数量字节的函数实现如下:
int recv_byte(int sock, char *buf, int len)
{
    int rc;
    int byte;
    for(byte = 0; byte < len; byte+=rc)
    {
        rc = recv(sock, buf+byte, len-byte, MSG_NOSIGNAL);
        if(rc == 0)
            break;
        if(rc < 0 && errno != EINTR)
        {
            byte = -1;
            break;
        }
    }
    return byte;
}
    其中sock参数用于接收socket描述符,参数buf指向存放接收到的数据缓冲区,参数len则是要接收的字节数。与发送操作不同的是有一个recv函数返回0的情况要处理,它表示在收到任何数据之前先收到了对端切断连接的通知。

2.3.5 可靠的发送与接受
    使用阻塞方式进行读写操作时,要注意读写过程被信号打断的问题,特别是当读写一个数据流socket时,如果连接已中断,则会发送SIGPIPE信号,此信号的默认处理是使进程退出,通常这不是期望的结果。解决这个问题的方法之一是捕获SIGPIPE信号,另一种方法是在发送或接受时传入MSG_NOSIGNAL参数,这样就不会发送SIGPIPE信号了。如:
send(sockfd,buf,len,MSG_NOSIGNAL)
    如果要可靠地发送指定数量的字节,必须循环进行发送,如:
/*可靠发送len个字节*/
int send_byte(int sock,const char *buf,int len)
{
    int rc;
    int byte;
    for(byte = 0; byte < len; byte+=rc)
    {  
        rc = send(sock,buf+byte,len-byte,MSG_NOSIGNAL);
        if(rc < 0 && errno != EINTR)
        {  
            byte = -1;
            break;
        }  
    }  
    return byte;
}
这个函数可以使用socket描述符sock可靠地发送len个字节,buf参数指向存放待发送数据的缓冲区。而可靠地接受指定数量字节的函数如:
/*可靠接受len个字节*/
int recv_byte(int sock, char *buf, int len)
{
    int rc;
    int byte;
    for(byte = 0; byte < len; byte+=rc)
    {
        rc = recv(sock, buf+byte, len-byte, MSG_NOSIGNAL);
        if(rc == 0)
            break;
        if(rc < 0 && errno != EINTR)
        {
            byte = -1;
            break;
        }
    }
    return byte;
}

2.4使用实例
该实例分为客户端和服务器端,其中服务器端首先建立起socket,然后调用本地端口的绑定,接着就开始与客户端建立联系,并接受客户端发送的消息。客户端则在建立socket之后调用connect函数来建立连接。
例子:
http://www.cs.ucsb.edu/~almeroth/classes/W01.176B/hw2/examples/
https://github.com/weimenlove/linux_app_program/tree/master/network
  评论这张
 
阅读(1912)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017