显示标签为“Socket”的博文。显示所有博文
显示标签为“Socket”的博文。显示所有博文

2007年9月4日星期二

Socket编程原理

什么是Socket
   Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。
    Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的 Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返 回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。常用的Socket类型有两种:流式Socket (SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据 报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

Socket建立
  为了建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄。socket函数原型为:
   int socket(int domain, int type, int protocol);
    domain指明所使用的协议族,通常为PF_INET,表示互联网协议族(TCP/IP协议族);type参数指定socket的类型: SOCK_STREAM 或SOCK_DGRAM,Socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议;protocol通常赋值"0"。 Socket()调用返回一个整型socket描述符,你可以在后面的调用使用它。
   Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用Socket函数时,socket执行体将建立一个Socket,实际上"建立一个Socket"意味着为一个Socket数据结构分配存储空间。Socket执行体为你管理描述符表。
  两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。Socket数据结构中包含这五种信息。

Socket配置
   通过socket调用返回一个socket描述符后,在使用socket进行网络传输以前,必须配置该socket。面向连接的socket客户端通过 调用Connect函数在socket数据结构中保存本地和远端信息。无连接socket的客户端和服务端以及面向连接socket的服务端通过调用 bind函数来配置本地信息。
Bind函数将socket与本机上的一个端口相关联,随后你就可以在该端口监听服务请求。Bind函数原型为:
   int bind(int sockfd,struct sockaddr *my_addr, int addrlen);
   Sockfd是调用socket函数返回的socket描述符,my_addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;addrlen常被设置为sizeof(struct sockaddr)。
   struct sockaddr结构类型是用来保存socket信息的:
   struct sockaddr {
   unsigned short sa_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14 字节的协议地址 */
};
   sa_family一般为AF_INET,代表Internet(TCP/IP)地址族;sa_data则包含该socket的IP地址和端口号。
   另外还有一种结构类型:
   struct sockaddr_in {
   short int sin_family; /* 地址族 */
   unsigned short int sin_port; /* 端口号 */
   struct in_addr sin_addr; /* IP地址 */
   unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */
   };
   这个结构更方便使用。sin_zero用来将sockaddr_in结构填充到与struct sockaddr同样的长度,可以用bzero()或memset()函数将其置为零。指向sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向 sockaddr_in的指针转换为指向sockaddr的指针;或者相反。
  使用bind函数时,可以用下面的赋值实现自动获得本机IP地址和随机获取一个没有被占用的端口号:
   my_addr.sin_port = 0; /* 系统随机选择一个未被使用的端口号 */
   my_addr.sin_addr.s_addr = INADDR_ANY; /* 填入本机IP地址 */
通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。
注意在使用bind函数是需要将sin_port和sin_addr转换成为网络字节优先顺序;而sin_addr则不需要转换。
  计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在Internet上传输数据时就需要进行转换,否则就会出现数据不一致。
   下面是几个字节顺序转换函数:
·htonl():把32位值从主机字节序转换成网络字节序
·htons():把16位值从主机字节序转换成网络字节序
·ntohl():把32位值从网络字节序转换成主机字节序
·ntohs():把16位值从网络字节序转换成主机字节序
   Bind()函数在成功被调用时返回0;出现错误时返回"-1"并将errno置为相应的错误号。需要注意的是,在调用bind函数时一般不要将端口号置为小于1024的值,因为1到1024是保留端口号,你可以选择大于1024中的任何一个没有被占用的端口号。

连接建立
  面向连接的客户程序使用Connect函数来配置socket并与远端服务器建立一个TCP连接,其函数原型为:
   int connect(int sockfd, struct sockaddr *serv_addr,int addrlen);
Sockfd 是socket函数返回的socket描述符;serv_addr是包含远端主机IP地址和端口号的指针;addrlen是远端地质结构的长度。 Connect函数在出现错误时返回-1,并且设置errno为相应的错误码。进行客户端程序设计无须调用bind(),因为这种情况下只需知道目的机器 的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到 打断口。
   Connect函数启动和远端主机的直接连接。只有面向连接的客户程序使用socket时才需要将此socket与远端主机相连。无连接协议从不建立直接连接。面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求。
   Listen函数使socket处于被动的监听模式,并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。
   int listen(int sockfd, int backlog);
Sockfd 是Socket系统调用返回的socket 描述符;backlog指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待accept()它们(参考下文)。Backlog对队列中等待 服务的请求的数目进行了限制,大多数系统缺省值为20。如果一个服务请求到来时,输入队列已满,该socket将拒绝连接请求,客户将收到一个出错信息。
当出现错误时listen函数返回-1,并置相应的errno错误码。
   accept()函数让服务器接收客户的连接请求。在建立好输入队列后,服务器就调用accept函数,然后睡眠并等待客户的连接请求。
   int accept(int sockfd, void *addr, int *addrlen);
    sockfd是被监听的socket描述符,addr通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息(某 台主机从某个端口发出该请求);addrten通常为一个指向值为sizeof(struct sockaddr_in)的整型指针变量。出现错误时accept函数返回-1并置相应的errno值。
  首先,当accept函数监视的 socket收到连接请求时,socket执行体将建立一个新的socket,执行体将这个新socket和请求连接进程的地址联系起来,收到服务请求的 初始socket仍可以继续在以前的 socket上监听,同时可以在新的socket描述符上进行数据传输操作。

数据传输
   Send()和recv()这两个函数用于面向连接的socket上进行数据传输。
   Send()函数原型为:
   int send(int sockfd, const void *msg, int len, int flags);
Sockfd是你想用来传输数据的socket描述符;msg是一个指向要发送数据的指针;Len是以字节为单位的数据的长度;flags一般情况下置为0(关于该参数的用法可参照man手册)。
   Send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当send()返回值与len不匹配时,应该对这种情况进行处理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
   recv()函数原型为:
   int recv(int sockfd,void *buf,int len,unsigned int flags);
   Sockfd是接受数据的socket描述符;buf 是存放接收数据的缓冲区;len是缓冲的长度。Flags也被置为0。Recv()返回实际上接收的字节数,当出现错误时,返回-1并置相应的errno值。
Sendto()和recvfrom()用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。
sendto()函数原型为:
   int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
  该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。Sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。
   Recvfrom()函数原型为:
   int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
    from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。Recvfrom()函数返回接收到的字节数或 当出现错误时返回-1,并置相应的errno。
如果你对数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。

结束传输
  当所有的数据操作结束以后,你可以调用close()函数来释放该socket,从而停止在该socket上的任何数据操作:
close(sockfd);
  你也可以调用shutdown()函数来关闭该socket。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。
   int shutdown(int sockfd,int how);
   Sockfd是需要关闭的socket的描述符。参数 how允许为shutdown操作选择以下几种方式:
   ·0-------不允许继续接收数据
   ·1-------不允许继续发送数据
   ·2-------不允许继续发送和接收数据,
   ·均为允许则调用close ()
   shutdown在操作成功时返回0,在出现错误时返回-1并置相应errno。

Socket编程实例
   代码实例中的服务器通过socket连接向客户端发送字符串"Hello, you are connected!"。只要在服务器上运行该服务器软件,在客户端运行客户软件,客户端就会收到该字符串。
   该服务器软件代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333 /*服务器监听端口号 */
#define BACKLOG 10 /* 最大同时连接请求数 */
main()
{
  int sockfd,client_fd; /*sock_fd:监听socket;client_fd:数据传输socket */
  struct sockaddr_in my_addr; /* 本机地址信息 */
  struct sockaddr_in remote_addr; /* 客户端地址信息 */
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
   perror("socket创建出错!"); exit(1);
  }
  my_addr.sin_family=AF_INET;
  my_addr.sin_port=htons(SERVPORT);
  my_addr.sin_addr.s_addr = INADDR_ANY;
  bzero(&(my_addr.sin_zero),8);
  if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {
    perror("bind出错!");
    exit(1);
  }
  if (listen(sockfd, BACKLOG) == -1) {
    perror("listen出错!");
    exit(1);
  }
  while(1) {
   sin_size = sizeof(struct sockaddr_in);
   if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1) {
      perror("accept出错");
      continue;
   }
   printf("received a connection from %s\n", inet_ntoa(remote_addr.sin_addr));
   if (!fork()) { /* 子进程代码段 */
     if (send(client_fd, "Hello, you are connected!\n", 26, 0) == -1)
     perror("send出错!");
     close(client_fd);
     exit(0);
   }
   close(client_fd);
   }
  }
}
   服务器的工作流程是这样的:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,然后调用 listen在相应的socket上监听,当accpet接收到一个连接服务请求时,将生成一个新的socket。服务器显示该客户机的IP地址,并通过 新的socket向客户端发送字符串"Hello,you are connected!"。最后关闭该socket。
  代码实例中的fork()函数生成一个子进程来处理数据传输部分,fork()语句对于子进程返回的值为0。所以包含fork函数的if语句是子进程代码部分,它与if语句后面的父进程代码部分是并发执行的。

客户端程序代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333
#define MAXDATASIZE 100 /*每次最大数据传输量 */
main(int argc, char *argv[]){
  int sockfd, recvbytes;
  char buf[MAXDATASIZE];
  struct hostent *host;
  struct sockaddr_in serv_addr;
  if (argc < 2) {
fprintf(stderr,"Please enter the server's hostname!\n");
exit(1);
}
  if((host=gethostbyname(argv[1]))==NULL) {
herror("gethostbyname出错!");
exit(1);
}
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket创建出错!");
exit(1);
}
  serv_addr.sin_family=AF_INET;
  serv_addr.sin_port=htons(SERVPORT);
  serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
  bzero(&(serv_addr.sin_zero),8);
  if (connect(sockfd, (struct sockaddr *)&serv_addr, \
   sizeof(struct sockaddr)) == -1) {
perror("connect出错!");
exit(1);
}
  if ((recvbytes=recv(sockfd, buf, MAXDATASIZE, 0)) ==-1) {
perror("recv出错!");
exit(1);
}
  buf[recvbytes] = '\0';
  printf("Received: %s",buf);
  close(sockfd);
}
  客户端程序首先通过服务器域名获得服务器的IP地址,然后创建一个socket,调用connect函数与服务器建立连接,连接成功之后接收从服务器发送过来的数据,最后关闭socket。
  函数gethostbyname()是完成域名转换的。由于IP地址难以记忆和读写,所以为了方便,人们常常用域名来表示主机,这就需要进行域名和IP地址的转换。函数原型为:
   struct hostent *gethostbyname(const char *name);
   函数返回为hosten的结构类型,它的定义如下:
   struct hostent {
  char *h_name; /* 主机的官方域名 */
   char **h_aliases; /* 一个以NULL结尾的主机别名数组 */
   int h_addrtype; /* 返回的地址类型,在Internet环境下为AF-INET */
   int h_length; /* 地址的字节长度 */
   char **h_addr_list; /* 一个以0结尾的数组,包含该主机的所有地址*/
   };
   #define h_addr h_addr_list[0] /*在h-addr-list中的第一个地址*/
   当 gethostname()调用成功时,返回指向struct hosten的指针,当调用失败时返回-1。当调用gethostbyname时,你不能使用perror()函数来输出错误信息,而应该使用herror()函数来输出。

  无连接的客户/服务器程序的在原理上和连接的客户/服务器是一样的,两者的区别在于无连接的客户/服务器中的客户一般不需要建立连接,而且在发送接收数据时,需要指定远端机的地址。

阻塞和非阻塞
   阻塞函数在完成其指定的任务以前不允许程序调用另一个函数。例如,程序执行一个读数据的函数调用时,在此函数完成读操作以前将不会执行下一程序语句。当 服务器运行到accept语句时,而没有客户连接服务请求到来,服务器就会停止在accept语句上等待连接服务请求的到来。这种情况称为阻塞 (blocking)。而非阻塞操作则可以立即完成。比如,如果你希望服务器仅仅注意检查是否有客户在等待连接,有就接受连接,否则就继续做其他事情,则 可以通过将Socket设置为非阻塞方式来实现。非阻塞socket在没有客户在等待时就使accept调用立即返回。
   #include
   #include
   ……
sockfd = socket(AF_INET,SOCK_STREAM,0);
fcntl(sockfd,F_SETFL,O_NONBLOCK);
……
   通过设置socket为非阻塞方式,可以实现"轮询"若干Socket。当企图从一个没有数据等待处理的非阻塞Socket读入数据时,函数将立即返 回,返回值为-1,并置errno值为EWOULDBLOCK。但是这种"轮询"会使CPU处于忙等待方式,从而降低性能,浪费系统资源。而调用 select()会有效地解决这个问题,它允许你把进程本身挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件 描述符上出现活动,select()调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费 CPU开销。Select函数原型为:
int select(int numfds,fd_set *readfds,fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);
   其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合。如果你希望确定是否可以 从标准输入和某个socket描述符读取数据,你只需要将标准输入的文件描述符0和相应的sockdtfd加入到readfds集合中;numfds的值 是需要检查的号码最高的文件描述符加1,这个例子中numfds的值应为sockfd+1;当select返回时,readfds将被修改,指示某个文件 描述符已经准备被读取,你可以通过FD_ISSSET()来测试。为了实现fd_set中对应的文件描述符的设置、复位和测试,它提供了一组宏:
   FD_ZERO(fd_set *set)----清除一个文件描述符集;
   FD_SET(int fd,fd_set *set)----将一个文件描述符加入文件描述符集中;
   FD_CLR(int fd,fd_set *set)----将一个文件描述符从文件描述符集中清除;
   FD_ISSET(int fd,fd_set *set)----试判断是否文件描述符被置位。
   Timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout长时间后没有文件描述符准备好即返回。struct timeval数据结构为:
   struct timeval {
   int tv_sec; /* seconds */
   int tv_usec; /* microseconds */
};

POP3客户端实例
  下面的代码实例基于POP3的客户协议,与邮件服务器连接并取回指定用户帐号的邮件。与邮件服务器交互的命令存储在字符串数组POPMessage中,程序通过一个do-while循环依次发送这些命令。
#include
#include
#include
#include
#include
#include
#include
#include
#define POP3SERVPORT 110
#define MAXDATASIZE 4096

main(int argc, char *argv[]){
int sockfd;
struct hostent *host;
struct sockaddr_in serv_addr;
char *POPMessage[]={
"USER userid\r\n",
"PASS password\r\n",
"STAT\r\n",
"LIST\r\n",
"RETR 1\r\n",
"DELE 1\r\n",
"QUIT\r\n",
NULL
};
int iLength;
int iMsg=0;
int iEnd=0;
char buf[MAXDATASIZE];

if((host=gethostbyname("your.server"))==NULL) {
perror("gethostbyname error");
exit(1);
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket error");
exit(1);
}
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(POP3SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if (connect(sockfd, (struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1){
perror("connect error");
exit(1);
}

do {
send(sockfd,POPMessage[iMsg],strlen(POPMessage[iMsg]),0);
printf("have sent: %s",POPMessage[iMsg]);

iLength=recv(sockfd,buf+iEnd,sizeof(buf)-iEnd,0);
iEnd+=iLength;
buf[iEnd]='\0';
printf("received: %s,%d\n",buf,iMsg);

iMsg++;
} while (POPMessage[iMsg]);

close(sockfd);
}

Select()系统调用及文件描述符集fd_set的应用

Select()系统调用及文件描述符集fd_set的应用

在网络程序中,一个进程同时处理多个文件描述符是很常见的情况。select()系统调用可以使进程检测同时等待的多个I/O设备,当没有设备准备好时,select()阻塞,其中任一设备准备好时,select()就返回。
select()的调用形式为:
#include
#include
int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout);
select 的第一个参数是文件描述符集中要被检测的比特数,这个值必须至少比待检测的最大文件描述符大1;参数readfds指定了被读监控的文件描述符集;参数 writefds指定了被写监控的文件描述符集;而参数exceptfds指定了被例外条件监控的文件描述符集。
参数timeout起了定时器的作用:到了指定的时间,无论是否有设备准备好,都返回调用。timeval的结构定义如下:
struct timeval{
long tv_sec; //表示几秒
long tv_usec; //表示几微妙
}
timeout取不同的值,该调用就表现不同的性质:
1.timeout为0,调用立即返回;
2.timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪;
3.timeout为正整数,就是一般的定时器。
select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。select的返回值有如下情况:
1.正常情况下返回就绪的文件描述符个数;
2.经过了timeout时长后仍无设备准备好,返回值为0;
3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4.如果出错,返回-1并设置相应的errno。
系统提供了4个宏对描述符集进行操作:
#include
#include
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_ISSET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
宏FD_SET 设置文件描述符集fdset中对应于文件描述符fd的位(设置为1),宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为 0),宏FD_ZERO清除文件描述符集fdset中的所有位(既把所有位都设置为0)。使用这3个宏在调用select前设置描述符屏蔽位,在调用 select后使用FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。
过去,描述符集被作为一个整数位屏 蔽码得到实现,但是这种实现对于多于32个的文件描述符将无法工作。描述符集现在通常用整数数组中的位域表示,数组元素的每一位对应一个文件描述符。例 如,一个整数占32位,那么整数数组的第一个元素代表文件描述符0到31,数组的第二个元素代表文件描述符32到63,以此类推。宏FD_SET设置整数 数组中对应于fd文件描述符的位为1,宏FD_CLR设置整数数组中对应于fd文件描述符的位为0,宏FD_ZERO设置整数数组中的所有位都为0。假设 执行如下程序后:
#include
#include
fd_set readset;
FD_ZERO(&readset);
FD_SET(5, &readset);
FD_SET(33, &readset);
则文件描述符集readset中对应于文件描述符6和33的相应位被置为1,如图1所示:

再执行如下程序后:
FD_CLR(5, &readset);
则文件描述符集readset对应于文件描述符6的相应位被置为0,如图2所示:

通常,操作系统通过宏FD_SETSIZE来声明在一个进程中select所能操作的文件描述符的最大数目。例如:
在4.4BSD的头文件中我们可以看到:
#ifndef FD_SETSIZE
#define FD_SETSIZE 1024
#endif
在红帽Linux的头文件中我们可以看到:
#define __FD_SETSIZE 1024
以及在头文件中我们可以看到:
#include
#define FD_SETSIZE __FD_SETSIZE
既 定义FD_SETSIZE为1024,一个整数占4个字节,既32位,那么就是用包含32个元素的整数数组来表示文件描述符集。我们可以在头文件中修改这 个值来改变select使用的文件描述符集的大小,但是必须重新编译内核才能使修改后的值有效。当前版本的unix操作系统没有限制FD_SETSIZE 的最大值,通常只受内存以及系统管理上的限制。
我们明白了文件描述符集的实现机制之后,就可对其进行灵活运用。(以下程序在红帽Linux 6.0下运行通过,函数fd_isempty用于判断文件描述符集是否为空;函数fd_fetch取出文件描述符集中的所有文件描述符)
#include
#include
#include
#include
struct my_fd_set{
fd_set fs; //定义文件描述符集fs
unsigned int nconnect; //文件描述符集fs中文件描述符的个数
unsigned int nmaxfd; //文件描述符集fs中最大的文件描述符
};
/* 函数fd_isempty用于判断文件描述符集是否为空,为空返回1,不为空则返回0 */
int fd_isempty(struct my_fd_set *pfs)
{
int i;
/* 文件描述符集fd_set是通过整数数组来实现的,所以定义整数数组myset的元素个数为文件描述符集fd_set所占内存空间的字节数除以整数所占内存空间的字节数。
*/
unsigned int myset[sizeof(fd_set) / sizeof(int)];
/* 把文件描述符集pfs->fs 拷贝到数组myset */
memcpy(myset, &pfs->fs, sizeof(fd_set));
for(i = 0; i < sizeof(fd_set) / sizeof(int); i++)
/* 如果myset的某个元素不为0,说明文件描述符集不为空,则函数返回0 */
if (myset[i])
return 0;
return 1; /* 如果myset的所有元素都为0,说明文件描述符集为空,则函数返回1 */
}
/* 函数fd_fetch对文件描述符集进行位操作,把为1的位换算成相应的文件描述符,然后就可对其进行I/O操作 */
void fd_fetch(struct my_fd_set *pfs)
{
struct my_fd_set *tempset; //定义一个临时的结构指针
unsigned int myset[sizeof(fd_set)/sizeof(unsigned int)];
unsigned int i, nbit, nfind, ntemp;
tempset = pfs;
memcpy(myset, &tempset->fs, sizeof(fd_set));
/* 把最大的文件描述符maxfd除以整数所占的位数,得出maxfd在文件描述符集中相应的位对应于整数数组myset的相应元素的下标,目的是为了减少检索的次数 */

nfind = tempset->nmaxfd / (sizeof(int)*8);
for (i = 0; i <= nfind; i++) {
/* 如果数组myset的某个元素为0,说明这个元素所对应的文件描述符集的32位全为0,则继续判断下一元素。*/
if (myset[i] == 0) continue;
/* 如果数组myset的某个元素不为0,说明这个元素所对应的文件描述符集的32位中有为1的,把myset[i]赋值给临时变量ntemp,对ntemp进行位运算,把为1的位换算成相应的文件描述符 */
ntemp = myset[i];
/* nbit记录整数的二进制位数,对ntemp从低到高位进行&1运算,直到整数的最高位,或直到文件描述符集中文件描述符的个数等于0 */
for (nbit = 0; tempset->nconnect && (nbit < sizeof(int)*8); nbit++) {
if (ntemp & 1) {
/* 如果某位为1,则可得到对应的文件描述符为nbit + 32*I,然后我们可对其进行I/O操作。这里我只是做了简单的显示。*/
printf("i = %d, nbit = %d, The file description is %d\n", i, nbit, nbit + 32*i);
/* 取出一个文件描述符后,将文件描述符集中文件描述符的个数减1 */
tempset->nconnect--; }
ntemp >>= 1; // ntemp右移一位
}
}
}

/* 下面的主程序是对以上两个函数的测试 */
main()
{
/* 假设fd1,fd2,fd3为3个文件描述符,实际运用中可为Socket描述符等 */
int fd1 = 7, fd2 = 256, fd3 = 1023, isempty;
struct my_fd_set connect_set;
connect_set.nconnect = 0;
connect_set.nmaxfd = 0;
FD_ZERO(&connect_set.fs);
/* FD_SET操作前对函数fd_isempty进行测试 */
isempty = fd_isempty(&connect_set);
printf("isempty = %d\n", isempty);
FD_SET(fd1, &connect_set.fs);
FD_SET(fd2, &connect_set.fs);
FD_SET(fd3, &connect_set.fs);
connect_set.nconnect = 3;
connect_set.nmaxfd = fd3 ;
/* FD_SET操作后,既把文件描述符加入到文件描述符集之后,对函数fd_isempty进行测试 */
isempty = fd_isempty(&connect_set);
printf("isempty = %d\n", isempty);
/* 对函数fd_ fetch进行测试 */
fd_fetch(&connect_set);
}

/* 程序输出结果为 :*/
isempty is 1
isempty is 0
i = 0, nbit = 7, The file description is 7
i = 8, nbit = 0, The file description is 256
i = 31, nbit = 31, The file description is 102

socket相关函数

socket相关函数
1.
getpeername()
简述:
获取与套接口相连的端地址。

#include

int PASCAL FAR getpeername( SOCKET s, struct sockaddr FAR* name,
int FAR* namelen);

s:标识一已连接套接口的描述字。
name:接收端地址的名字结构。
namelen:一个指向名字结构的指针。

注释:
getpeername()函数用于从端口s中获取与它捆绑的端口名,并把它存放在sockaddr类型的name结构中。它适用于数据报或流类套接口。

返回值:
若无错误发生,getpeername()返回0。否则的话,返回SOCKET_ERROR,应用程序可通过WSAGetLastError()来获取相应的错误代码。

错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAEFAULT:namelen参数不够大。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAENOTCONN 套接口未连接。
WSAENOTSOCK:描述字不是一个套接口。

参见:
bind(), socket(), getsockname().
我扬帆,不为到达终点,只为这片海洋

htonl()表示将32位的主机字节顺序转化为32位的网络字节顺序 htons()表示将16位的主机字节顺序转化为16位的网络字节顺序(ip地址是32位的端口号是16位的 )

inet_ntoa()
简述:
将网络地址转换成“.”点隔的字符串格式。

#include

char FAR* PASCAL FAR inet_ntoa( struct in_addr in);

in:一个表示Internet主机地址的结构。

注释:
本函数将一个用in参数所表示的Internet地址结构转换成以“.” 间隔的诸如“a.b.c.d”的字符串形式。请注意inet_ntoa()返回的字符串存放在WINDOWS套接口实现所分配的内存中。应用程序不应假设该内存是如何分配的。在同一个线程的下一个WINDOWS套接口调用前,数据将保证是有效。

返回值:
若无错误发生,inet_ntoa()返回一个字符指针。否则的话,返回NVLL。其中的数据应在下一个WINDOWS套接口调用前复制出来。

参见:
inet_addr().

测试代码如下
include
#include
#include
#include
#include
int main(int aargc, char* argv[])
{
struct in_addr addr1,addr2;
ulong l1,l2;
l1= inet_addr("192.168.0.74");
l2 = inet_addr("211.100.21.179");
memcpy(&addr1, &l1, 4);
memcpy(&addr2, &l2, 4);

printf("%s : %s\n", inet_ntoa(addr1), inet_ntoa(addr2)); //注意这一句的运行结果

printf("%s\n", inet_ntoa(addr1));
printf("%s\n", inet_ntoa(addr2));
return 0;
}
实际运行结果如下:
192.168.0.74 : 192.168.0.74 //从这里可以看出,printf里的inet_ntoa只运行了一次。
192.168.0.74
211.100.21.179

inet_ntoa返回一个char *,而这个char *的空间是在inet_ntoa里面静态分配的,所以inet_ntoa后面的调用会覆盖上一次的调用。第一句printf的结果只能说明在printf里面的可变参数的求值是从右到左的,仅此而已。

网络字节序与主机字节序
网络字节序与主机字节序

不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序
最常见的有两种
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址

LE little-endian
最符合人的思维的字节序
地址低位存储值的低位
地址高位存储值的高位
怎么讲是最符合人的思维的字节序,是因为从人的第一观感来说
低位值小,就应该放在内存地址小的地方,也即内存地址低位
反之,高位值就应该放在内存地址大的地方,也即内存地址高位

BE big-endian
最直观的字节序
地址低位存储值的高位
地址高位存储值的低位
为什么说直观,不要考虑对应关系
只需要把内存地址从左到右按照由低到高的顺序写出
把值按照通常的高位到低位的顺序写出
两者对照,一个字节一个字节的填充进去

例子:在内存中双字0x01020304(DWORD)的存储方式

内存地址
4000 4001 4002 4003
LE 04 03 02 01
BE 01 02 03 04

例子:如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为
big-endian little-endian
0x0000 0x12 0xcd
0x0001 0x23 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12
x86系列CPU都是little-endian的字节序.

网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

为了进行转换 bsd socket提供了转换的函数 有下面四个
htons 把unsigned short类型从主机序转换到网络序
htonl 把unsigned long类型从主机序转换到网络序
ntohs 把unsigned short类型从网络序转换到主机序
ntohl 把unsigned long类型从网络序转换到主机序

在使用little endian的系统中 这些函数会把字节序进行转换
在使用big endian类型的系统中 这些函数会定义成空宏

同样 在网络程序开发时 或是跨平台开发时 也应该注意保证只用一种字节序 不然两方的解释不一样就会产生bug.

注:
1、网络与主机字节转换函数:htons ntohs htonl ntohl (s 就是short l是long h是host n是network)
2、不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表。
处理器 操作系统 字节排序
Alpha 全部 Little endian
HP-PA NT Little endian
HP-PA UNIX Big endian
Intelx86 全部 Little endian <-----x86系统是小端字节序系统
Motorola680x() 全部 Big endian
MIPS NT Little endian
MIPS UNIX Big endian
PowerPC NT Little endian
PowerPC 非NT Big endian <-----PPC系统是大端字节序系统
RS/6000 UNIX Big endian
SPARC UNIX Big endian
IXP1200 ARM核心 全部 Little endian
关于网络字节序和主机字节序的转换
字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有两种字节顺序,根据他们所处的位置我们分别称为主机节序和网络字节序

通常我们认为网络字节序为标准顺序,封包的时候,将主机字节序转换为网络字节序,拆包的时候要将网络字节序转换为主机字节序。原以为还要自己写函数,其实网络库已经提供了。

主机到网络:short/int/long IPAddress.HostToNetworkOrder(short/int/long)

网络到主机:short/int/long IPAddress.NetworkToHostOrder(short/int/long)



主机字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处,如:

int x=1; //此时x为主机字节序:[1][0][0][0] 低位到高位

int y=65536 //此时y为主机字节序:[0][0][1][0] 低位到高位

我们通过主机到网络字节序的转换函数分别对x和y进行转换得到他们对应的网络字节序值,网络节序是高字节数据存放在低地址处,低字节数据存放在高地址处,如:

int m=IPAddress.HostToNetworkOrder(x);

//此时m为主机字节序:[0][0][0][1] 高位到低位

int n=IPAddress.HostToNetworkOrder(y);

//此时n为主机字节序:[0][1][0][0] 高位到低位



经过转换以后,我们就可以通过

byte[]btValue=BitConverter.GetBytes(m);

得到一个长度为4的byte数组,然后将这个数组设置到报文的相应位置发送出去即可。

同样,收到报文后,可以将报文按域拆分,得到btValue,使用

int m=BitConverter.ToInt32(btValue,0);//从btValue的第0位开始转换

得到该域的值,此时还不能直接使用,应该再用网络到主机字节序的转换函数进行转换:

int x=IPAddress.NetworkToHostOrder(m);

这时得到的x才是报文中的实际值。
3.
getsockname()
简述:
获取一个套接口的本地名字。

#include

int PASCAL FAR getsockname( SOCKET s, struct sockaddr FAR* name,
int FAR* namelen);

s:标识一个已捆绑套接口的描述字。
name:接收套接口的地址(名字)。
namelen:名字缓冲区长度。

注释:
getsockname()函数用于获取一个套接口的名字。它用于一个已捆绑或已连接套接口s,本地地址将被返回。本调用特别适用于如下情况:未调用bind()就调用了connect(),这时唯有getsockname()调用可以获知系统内定的本地地址。在返回时,namelen参数包含了名字的实际字节数。
若一个套接口与INADDR_ANY捆绑,也就是说该套接口可以用任意主机的地址,此时除非调用connect()或accept()来连接,否则getsockname()将不会返回主机IP地址的任何信息。除非套接口被连接,WINDOWS套接口应用程序不应假设IP地址会从INADDR_ANY变成其他地址。这是因为对于多个主机环境下,除非套接口被连接,否则该套接口所用的IP地址是不可知的。

返回值:
若无错误发生,getsockname()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。


错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAEFAULT:namelen参数不够大。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAENOTSOCK:描述字不是一个套接口。
WSAEINVAL:套接口未用bind()捆绑。

参见:
bind(), socket(), getpeername().

2007年8月20日星期一

自己封装udp接口,实现win32,Linux,solaris,FreeBSD统一调用!




/*****test.c****/
/*
@Author:duanjigang<duanjigang1983@gmail.com>
@CopyRight@2005-2006

It is a easy job to use this program:
For diffrent platform there are diffrent methods to complie and link
Windows: complie it with vc++ 6.0 IDE
RedHat Linux: gcc -o test test.c
Soloaris9: gcc -o test test.c -lsocket -lresolv
FreeBsd: gcc -o test test.c
*/

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>
#include <sys/types.h>
#ifdef _WIN32
#define SLEEP(t) Sleep(1000*t)
#include <Winsock2.h>
#include <Windows.h>
#pragma comment(lib,"Ws2_32.lib")
#else
/*
for unix platform Like Linux8,9 and Fedoral ,Solaris9 and freebsd
*/
#define SLEEP(t) sleep(t)
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
#endif

#ifdef __FreeBSD__
#include <netinet/in.h>
#include <arpa/inet.h>
#endif

#ifdef __SunOS__
/*
you need to link this file with option -lsocket -lresolv
gcc -o test test.c -lsocket -lresolv
*/
#endif
#define MSG_SIZE 1024
int initSndSock(struct sockaddr_in * addr,char * SERVER_IP,int port);
int SndToSock(int sockfd,struct sockaddr_in * addr,char buffer[]);
int RcvFrmSock(int sockfd, char buffer[]);
int initRcvSockAtPort(int port);
int main(void)
{
int sockfd;
struct sockaddr_in addr;
char buffer[MSG_SIZE];

/*
can be used an server to receive message
*/
/*

sockfd = initRcvSockAtPort(9001);
while(1)
{
memset(buffer, 0, MSG_SIZE);
if(RcvFrmSock(sockfd, buffer)>=0)
printf("Receive : %s", buffer);

}
*/

/*
can be used as client to send messge
to test receive functions ,comment code below and delete comment
flags of codes above ,then run again
*/
sockfd = initSndSock(&addr, "192.168.1.124",9001);
while(1)
{

memset(buffer, 0, MSG_SIZE);
#ifdef _WIN32
sprintf(buffer, "%s", "I am windows message");

printf("WIN 32 Sending once ..........\n");
#else
sprintf(buffer, "%s", "I am not Windows message");

printf("Other Unix system Sending once ..........\n");
#endif
SndToSock(sockfd, &addr, buffer);
SLEEP(1);
}


}
int initSndSock(struct sockaddr_in * addr,char * SERVER_IP,int port)
{
int sockfd;
int a1,a2,a3,a4;
#ifdef WIN32
WSADATA w;
if(WSAStartup(0x0101, &w) != 0)
{
fprintf(stderr, "Could not open Windows connection.\n");
exit(0);
}
#endif
sockfd=socket(AF_INET,SOCK_DGRAM,0);
#ifdef WIN32
if(sockfd == INVALID_SOCKET)
{
fprintf(stderr, "Could not create socket.\n");
WSACleanup();
exit(0);
}
memset((void *)addr, '\0', sizeof(struct sockaddr_in));
#else
if(sockfd<0)
{
fprintf(stderr,"Socket Error:%s\n",strerror(errno));
return -1;
}
bzero(addr,sizeof(struct sockaddr_in));
#endif
addr->sin_family=AF_INET;
addr->sin_port=htons((u_short)port);
#ifdef _WIN32

sscanf(SERVER_IP,"%d.%d.%d.%d",&a1,&a2,&a3,&a4);
addr->sin_addr.S_un.S_un_b.s_b1 = (unsigned char)a1;
addr->sin_addr.S_un.S_un_b.s_b2 = (unsigned char)a2;
addr->sin_addr.S_un.S_un_b.s_b3 = (unsigned char)a3;
addr->sin_addr.S_un.S_un_b.s_b4 = (unsigned char)a4;

#else
if(inet_aton(SERVER_IP,&(addr->sin_addr))<0)
{
fprintf(stderr,"Ip error:%s\n",strerror(errno));
return -1;
}
#endif
return sockfd;
}

int SndToSock(int sockfd,struct sockaddr_in * addr,char buffer[])
{
int len = sizeof(struct sockaddr_in);
sendto(sockfd,buffer,strlen(buffer),0,(struct sockaddr*)addr,len);
return 0;
}

int RcvFrmSock(int sockfd, char buffer[])
{
int n,len,ll;
struct sockaddr_in addr;
len = sizeof(struct sockaddr_in);
ll = len;
memset(buffer, '\0' , MSG_SIZE);
n=recvfrom(sockfd, buffer, MSG_SIZE, 0, (struct sockaddr*)&addr, &ll);
if(n < 0)
{
return -1;
}
buffer[n] = '\0';
return n;
}

int initRcvSockAtPort(int port)
{
int sockfd;
struct sockaddr_in addr;
#ifdef _WIN32
WSADATA w;
if (WSAStartup(0x0101, &w) != 0)
{
fprintf(stderr, "Could not open Windows connection.\n");
exit(0);
}
#endif
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
#ifdef _WIN32
WSACleanup();
#endif
fprintf(stderr, "Socket Error:%s\n", strerror(errno));
return -1;
}

memset((void*)&addr, '\0', sizeof(struct sockaddr_in) );
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htons(INADDR_ANY);

addr.sin_port = htons((u_short )port);
if(bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0)
{
fprintf(stderr, "Bind Error :%s\n", strerror(errno));
return -1;
}
return sockfd;
}

2007年7月31日星期二

setsockopt函数

int setsockopt (
SOCKET s,
int level,
int optname,
const char FAR * optval,
int optlen
);

The Windows Sockets setsockopt function sets a socket option.

中文解释好像是:设置套接字的选项。

先看如下代码:
setsockopt(SockRaw,IPPROTO_IP,IP_HDRINCL,(char *)&flag,sizeof(int))

这里是设置SockRaw这个套接字的ip选项中的IP_HDRINCL

参考以下资料:

***************************************************************************************************

Linux网络编程--8. 套接字选项

有时候我们要控制套接字的行为(如修改缓冲区的大小),这个时候我们就要控制套接字的选项了. 

8.1 getsockopt和setsockopt 

int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen)
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t *optlen)

level指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项. 
optname指定控制的方式(选项的名称),我们下面详细解释 

optval获得或者是设置套接字选项.根据选项名称的数据类型进行转换 

选项名称        说明                  数据类型
========================================================================
            SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST      允许发送广播数据            int
SO_DEBUG        允许调试                int
SO_DONTROUTE      不查找路由               int
SO_ERROR        获得套接字错误             int
SO_KEEPALIVE      保持连接                int
SO_LINGER        延迟关闭连接              struct linger
SO_OOBINLINE      带外数据放入正常数据流         int
SO_RCVBUF        接收缓冲区大小             int
SO_SNDBUF        发送缓冲区大小             int
SO_RCVLOWAT       接收缓冲区下限             int
SO_SNDLOWAT       发送缓冲区下限             int
SO_RCVTIMEO       接收超时                struct timeval
SO_SNDTIMEO       发送超时                struct timeval
SO_REUSERADDR      允许重用本地地址和端口         int
SO_TYPE         获得套接字类型             int
SO_BSDCOMPAT      与BSD系统兼容              int
==========================================================================
            IPPROTO_IP
--------------------------------------------------------------------------
IP_HDRINCL       在数据包中包含IP首部          int
IP_OPTINOS       IP首部选项               int
IP_TOS         服务类型
IP_TTL         生存时间                int
==========================================================================
            IPPRO_TCP
--------------------------------------------------------------------------
TCP_MAXSEG       TCP最大数据段的大小           int
TCP_NODELAY       不使用Nagle算法             int
=========================================================================

关于这些选项的详细情况请查看 Linux Programmer"s Manual 

8.2 ioctl 
ioctl可以控制所有的文件描述符的情况,这里介绍一下控制套接字的选项. 

int ioctl(int fd,int req,...)
 
==========================================================================
            ioctl的控制选项
--------------------------------------------------------------------------
SIOCATMARK       是否到达带外标记            int
FIOASYNC        异步输入/输出标志            int
FIONREAD        缓冲区可读的字节数           int
==========================================================================

详细的选项请用 man ioctl_list 查看. 





int setsockopt(
SOCKET s,
int level,
int optname,
const char* optval,
int optlen
);

s(套接字): 指向一个打开的套接口描述字
level:(级别): 指定选项代码的类型。
SOL_SOCKET: 基本套接口
IPPROTO_IP: IPv4套接口
IPPROTO_IPV6: IPv6套接口
IPPROTO_TCP: TCP套接口
optname(选项名): 选项名称
optval(选项值): 是一个指向变量的指针 类型:整形,套接口结构, 其他结构类型:linger{}, timeval{ }
optlen(选项长度) :optval 的大小

返回值:标志打开或关闭某个特征的二进制选项
[/code:1:59df4ce128]

========================================================================
SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST 允许发送广播数据 int
适用於 UDP socket。其意义是允许 UDP socket 「广播」(broadcast)讯息到网路上。

SO_DEBUG 允许调试 int

SO_DONTROUTE 不查找路由 int

SO_ERROR 获得套接字错误 int

SO_KEEPALIVE 保持连接 int
检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。 设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方 发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节.它会导致以下三种情况: 对方接收一切正常:以期望的 ACK响应。2小时后,TCP将发出另一个探测分节。 对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接 口本身则被关闭。 对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到 一个响应。在发出第一个探测分节11分钟15秒后若仍无响应就放弃。套接口的待处理错 误被置为ETIMEOUT,套接口本身则被关闭。如ICMP错误是“host unreachable (主机不 可达)”,说明对方主机并没有崩溃,但是不可达,这种情况下待处理错误被置为 EHOSTUNREACH。

SO_DONTLINGER 若为真,则SO_LINGER选项被禁止。
SO_LINGER 延迟关闭连接 struct linger
上面这两个选项影响close行为
选项 间隔 关闭方式 等待关闭与否
SO_DONTLINGER 不关心 优雅 否
SO_LINGER 零 强制 否
SO_LINGER 非零 优雅 是
若设置了SO_LINGER(亦即linger结构中的l_onoff域设为非零,参见2.4,4.1.7和4.1.21各节),并设置了零超时间隔,则 closesocket()不被阻塞立即执行,不论是否有排队数据未发送或未被确认。这种关闭方式称为“强制”或“失效”关闭,因为套接口的虚电路立即被复位,且丢失了未发送的数据。在远端的recv()调用将以WSAECONNRESET出错。
若设置了SO_LINGER并确定了非零的超时间隔,则closesocket()调用阻塞进程,直到所剩数据发送完毕或超时。这种关闭称为“优雅的”关闭。请注意如果套接口置为非阻塞且 SO_LINGER设为非零超时,则closesocket()调用将以WSAEWOULDBLOCK错误返回。
若在一个流类套接口上设置了 SO_DONTLINGER(也就是说将linger结构的l_onoff域设为零;参见2.4,4.1.7,4.1.21节),则 closesocket()调用立即返回。但是,如果可能,排队的数据将在套接口关闭前发送。请注意,在这种情况下WINDOWS套接口实现将在一段不确定的时间内保留套接口以及其他资源,这对于想用所以套接口的应用程序来说有一定影响。

SO_OOBINLINE 带外数据放入正常数据流,在普通数据流中接收带外数据 int

SO_RCVBUF 接收缓冲区大小 int
设置接收缓冲区的保留大小
与 SO_MAX_MSG_SIZE 或TCP滑动窗口无关,如果一般发送的包很大很频繁,那么使用这个选项

SO_SNDBUF 发送缓冲区大小 int
设置发送缓冲区的保留大小
与 SO_MAX_MSG_SIZE 或TCP滑动窗口无关,如果一般发送的包很大很频繁,那么使用这个选项
每个套接口都有一个发送缓冲区和一个接收缓冲区。 接收缓冲区被TCP和UDP用来将接收到的数据一直保存到由应用进程来读。 TCP:TCP通告另一端的窗口大小。 TCP套接口接收缓冲区不可能溢出,因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过宙口大小的数据,则接 收方TCP将丢弃它。 UDP:当接收到的数据报装不进套接口接收缓冲区时,此数据报就被丢弃。UDP是没有 流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的UDP丢弃数据报。

SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
每个套接口都有一个接收低潮限度和一个发送低潮限度。它们是函数selectt使用的, 接收低潮限度是让select返回“可读”而在套接口接收缓冲区中必须有的数据总量。 ——对于一个TCP或UDP套接口,此值缺省为1。发送低潮限度是让select返回“可写” 而在套接口发送缓冲区中必须有的可用空间。对于TCP套接口,此值常缺省为2048。 对于UDP使用低潮限度, 由于其发送缓冲区中可用空间的字节数是从不变化的,只要 UDP套接口发送缓冲区大小大于套接口的低潮限度,这样的UDP套接口就总是可写的。 UDP没有发送缓冲区,只有发送缓冲区的大小。

SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSERADDR 允许重用本地地址和端口 int
充许绑定已被使用的地址(或端口号),可以参考bind的man

SO_EXCLUSIVEADDRUSE
独占模式使用端口,就是不充许和其它程序使用SO_REUSEADDR共享的使用某一端口。
在确定多重绑定使用谁的时候,根据一条原则是谁的指定最明确则将包递交给谁,而且没有权限之分,也就是说低级权限的用户是可以重绑定在高级权限如服务启动的端口上的,这是非常重大的一个安全隐患,
如果不想让自己程序被监听,那么使用这个选项

SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int

==========================================================================
IPPROTO_IP
--------------------------------------------------------------------------
IP_HDRINCL 在数据包中包含IP首部 int
这个选项常用于黑客技术中,隐藏自己的IP地址

IP_OPTINOS IP首部选项 int
IP_TOS 服务类型
IP_TTL 生存时间 int

以下IPV4选项用于组播
IPv4 选项 数据类型 描 述
  IP_ADD_MEMBERSHIP struct ip_mreq 加入到组播组中
  IP_ROP_MEMBERSHIP struct ip_mreq 从组播组中退出
  IP_MULTICAST_IF struct ip_mreq 指定提交组播报文的接口
  IP_MULTICAST_TTL u_char 指定提交组播报文的TTL
  IP_MULTICAST_LOOP u_char 使组播报文环路有效或无效
在头文件中定义了ip_mreq结构:
[code:1:63724de67f]
struct ip_mreq {
struct in_addr imr_multiaddr; /* IP multicast address of group */
struct in_addr imr_interface; /* local IP address of interface */
};
[/code:1:63724de67f]
若进程要加入到一个组播组中,用soket的setsockopt()函数发送该选项。该选项类型是ip_mreq结构,它的第一个字段imr_multiaddr指定了组播组的地址,第二个字段imr_interface指定了接口的IPv4地址。
  IP_DROP_MEMBERSHIP
  该选项用来从某个组播组中退出。数据结构ip_mreq的使用方法与上面相同。
  IP_MULTICAST_IF
  该选项可以修改网络接口,在结构ip_mreq中定义新的接口。
  IP_MULTICAST_TTL
  设置组播报文的数据包的TTL(生存时间)。默认值是1,表示数据包只能在本地的子网中传送。
  IP_MULTICAST_LOOP
  组播组中的成员自己也会收到它向本组发送的报文。这个选项用于选择是否激活这种状态。

无双 回复于:2003-05-08 21:21:52

IPPRO_TCP
--------------------------------------------------------------------------
TCP_MAXSEG TCP最大数据段的大小 int
获取或设置TCP连接的最大分节大小(MSS)。返回值是我们的TCP发送给另一端的最大 数据量,它常常就是由另一端用SYN分节通告的MSS,除非我们的TCP选择使用一个比 对方通告的MSS小些的值。如果此值在套接口连接之前取得,则返回值为未从另·—端 收到Mss选项的情况下所用的缺省值。小于此返回值的信可能真正用在连接上,因为譬 如说使用时间戳选项的话,它在每个分节上占用12字节的TCP选项容量。我们的TcP将 发送的每个分节的最大数据量也可在连接存活期内改变,但前提是TCP要支持路径MTU 发现功能。如果到对方的路径改变了,此值可上下调整。
TCP_NODELAY 不使用Nagle算法 int

指定TCP开始发送保持存活探测分节前以秒为单位的连接空闲时间。缺省值至少必须为7200秒,即2小时。此选项仅在SO_KEPALIVEE套接口选项打开时才有效。

TCP_NODELAY 和 TCP_CORK,
这两个选项都对网络连接的行为具有重要的作用。许多UNIX系统都实现了TCP_NODELAY选项,但是,TCP_CORK则是Linux系统所独有的而且相对较新;它首先在内核版本2.4上得以实现。此外,其他UNIX系统版本也有功能类似的选项,值得注意的是,在某种由BSD派生的系统上的 TCP_NOPUSH选项其实就是TCP_CORK的一部分具体实现。
TCP_NODELAY和TCP_CORK基本上控制了包的 “Nagle化”,Nagle化在这里的含义是采用Nagle算法把较小的包组装为更大的帧。John Nagle是Nagle算法的发明人,后者就是用他的名字来命名的,他在1984年首次用这种方法来尝试解决福特汽车公司的网络拥塞问题(欲了解详情请参看IETF RFC 896)。他解决的问题就是所谓的silly window syndrome ,中文称“愚蠢窗口症候群”,具体含义是,因为普遍终端应用程序每产生一次击键操作就会发送一个包,而典型情况下一个包会拥有一个字节的数据载荷以及40个字节长的包头,于是产生4000%的过载,很轻易地就能令网络发生拥塞,。 Nagle化后来成了一种标准并且立即在因特网上得以实现。它现在已经成为缺省配置了,但在我们看来,有些场合下把这一选项关掉也是合乎需要的。
现在让我们假设某个应用程序发出了一个请求,希望发送小块数据。我们可以选择立即发送数据或者等待产生更多的数据然后再一次发送两种策略。如果我们马上发送数据,那么交互性的以及客户/服务器型的应用程序将极大地受益。例如,当我们正在发送一个较短的请求并且等候较大的响应时,相关过载与传输的数据总量相比就会比较低,而且,如果请求立即发出那么响应时间也会快一些。以上操作可以通过设置套接字的TCP_NODELAY选项来完成,这样就禁用了Nagle算法。
另外一种情况则需要我们等到数据量达到最大时才通过网络一次发送全部数据,这种数据传输方式有益于大量数据的通信性能,典型的应用就是文件服务器。应用 Nagle算法在这种情况下就会产生问题。但是,如果你正在发送大量数据,你可以设置TCP_CORK选项禁用Nagle化,其方式正好同 TCP_NODELAY相反(TCP_CORK 和 TCP_NODELAY 是互相排斥的)。下面就让我们仔细分析下其工作原理。
假设应用程序使用sendfile()函数来转移大量数据。应用协议通常要求发送某些信息来预先解释数据,这些信息其实就是报头内容。典型情况下报头很小,而且套接字上设置了TCP_NODELAY。有报头的包将被立即传输,在某些情况下(取决于内部的包计数器),因为这个包成功地被对方收到后需要请求对方确认。这样,大量数据的传输就会被推迟而且产生了不必要的网络流量交换。
但是,如果我们在套接字上设置了TCP_CORK(可以比喻为在管道上插入 “塞子”)选项,具有报头的包就会填补大量的数据,所有的数据都根据大小自动地通过包传输出去。当数据传输完成时,最好取消TCP_CORK 选项设置给连接“拔去塞子”以便任一部分的帧都能发送出去。这同“塞住”网络连接同等重要。
总而言之,如果你肯定能一起发送多个数据集合(例如HTTP响应的头和正文),那么我们建议你设置TCP_CORK选项,这样在这些数据之间不存在延迟。能极大地有益于WWW、FTP以及文件服务器的性能,同时也简化了你的工作。示例代码如下:

intfd, on = 1;

/* 此处是创建套接字等操作,出于篇幅的考虑省略*/

setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /* cork */
write (fd, …);
fprintf (fd, …);
sendfile (fd, …);
write (fd, …);
sendfile (fd, …);

on = 0;
setsockopt (fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /* 拔去塞子 */

不幸的是,许多常用的程序并没有考虑到以上问题。例如,Eric Allman编写的sendmail就没有对其套接字设置任何选项。

Apache HTTPD 是因特网上最流行的Web服务器,它的所有套接字就都设置了TCP_NODELAY选项,而且其性能也深受大多数用户的满意。这是为什么呢?答案就在于实现的差别之上。由BSD衍生的TCP/IP协议栈(值得注意的是FreeBSD)在这种状况下的操作就不同。当在TCP_NODELAY 模式下提交大量小数据块传输时,大量信息将按照一次write()函数调用发送一块数据的方式发送出去。然而,因为负责请求交付确认的记数器是面向字节而非面向包(在 Linux上)的,所以引入延迟的概率就降低了很多。结果仅仅和全部数据的大小有关系。而 Linux 在第一包到达之后就要求确认,FreeBSD则在进行如此操作之前会等待好几百个包。

在Linux系统上,TCP_NODELAY的效果同习惯于BSD TCP/IP协议栈的开发者所期望的效果有很大不同,而且在Linux上的Apache性能表现也会更差些。其他在Linux上频繁采用TCP_NODELAY的应用程序也有同样的问题。

TCP_DEFER_ACCEPT

我们首先考虑的第1个选项是TCP_DEFER_ACCEPT(这是Linux系统上的叫法,其他一些操作系统上也有同样的选项但使用不同的名字)。为了理解TCP_DEFER_ACCEPT选项的具体思想,我们有必要大致阐述一下典型的HTTP客户/服务器交互过程。请回想下TCP是如何与传输数据的目标建立连接的。在网络上,在分离的单元之间传输的信息称为IP包(或IP 数据报)。一个包总有一个携带服务信息的包头,包头用于内部协议的处理,并且它也可以携带数据负载。服务信息的典型例子就是一套所谓的标志,它把包标记代表TCP/IP协议栈内的特殊含义,例如收到包的成功确认等等。通常,在经过“标记”的包里携带负载是完全可能的,但有时,内部逻辑迫使TCP/IP协议栈发出只有包头的IP包。这些包经常会引发讨厌的网络延迟而且还增加了系统的负载,结果导致网络性能在整体上降低。
现在服务器创建了一个套接字同时等待连接。TCP/IP式的连接过程就是所谓“3次握手”。首先,客户程序发送一个设置SYN标志而且不带数据负载的TCP包(一个SYN包)。服务器则以发出带SYN/ACK标志的数据包(一个SYN/ACK包)作为刚才收到包的确认响应。客户随后发送一个ACK包确认收到了第2个包从而结束连接过程。在收到客户发来的这个SYN/ACK包之后,服务器会唤醒一个接收进程等待数据到达。当3次握手完成后,客户程序即开始把“有用的”的数据发送给服务器。通常,一个HTTP请求的量是很小的而且完全可以装到一个包里。但是,在以上的情况下,至少有4个包将用来进行双向传输,这样就增加了可观的延迟时间。此外,你还得注意到,在“有用的”数据被发送之前,接收方已经开始在等待信息了。
为了减轻这些问题所带来的影响,Linux(以及其他的一些操作系统)在其TCP实现中包括了TCP_DEFER_ACCEPT选项。它们设置在侦听套接字的服务器方,该选项命令内核不等待最后的ACK包而且在第1个真正有数据的包到达才初始化侦听进程。在发送SYN/ACK包之后,服务器就会等待客户程序发送含数据的IP包。现在,只需要在网络上传送3个包了,而且还显著降低了连接建立的延迟,对HTTP通信而言尤其如此。
这一选项在好些操作系统上都有相应的对等物。例如,在FreeBSD上,同样的行为可以用以下代码实现:

/* 为明晰起见,此处略去无关代码 */
struct accept_filter_arg af = { "dataready", "" };
setsockopt(s, SOL_SOCKET, SO_ACCEPTFILTER, &af, sizeof(af));
这个特征在FreeBSD上叫做“接受过滤器”,而且具有多种用法。不过,在几乎所有的情况下其效果与TCP_DEFER_ACCEPT是一样的:服务器不等待最后的ACK包而仅仅等待携带数据负载的包。要了解该选项及其对高性能Web服务器的重要意义的更多信息请参考Apache文档上的有关内容。
就HTTP 客户/服务器交互而言,有可能需要改变客户程序的行为。客户程序为什么要发送这种“无用的”ACK包呢?这是因为,TCP协议栈无法知道ACK包的状态。如果采用FTP而非HTTP,那么客户程序直到接收了FTP服务器提示的数据包之后才发送数据。在这种情况下,延迟的ACK将导致客户/服务器交互出现延迟。为了确定ACK是否必要,客户程序必须知道应用程序协议及其当前状态。这样,修改客户行为就成为必要了。
对Linux客户程序来说,我们还可以采用另一个选项,它也被叫做TCP_DEFER_ACCEPT。我们知道,套接字分成两种类型,侦听套接字和连接套接字,所以它们也各自具有相应的 TCP选项集合。因此,经常同时采用的这两类选项却具有同样的名字也是完全可能的。在连接套接字上设置该选项以后,客户在收到一个SYN/ACK包之后就不再发送ACK包,而是等待用户程序的下一个发送数据请求;因此,服务器发送的包也就相应减少了。

TCP_QUICKACK

阻止因发送无用包而引发延迟的另一个方法是使用TCP_QUICKACK选项。这一选项与 TCP_DEFER_ACCEPT不同,它不但能用作管理连接建立过程而且在正常数据传输过程期间也可以使用。另外,它能在客户/服务器连接的任何一方设置。如果知道数据不久即将发送,那么推迟ACK包的发送就会派上用场,而且最好在那个携带数据的数据包上设置ACK 标志以便把网络负载减到最小。当发送方肯定数据将被立即发送(多个包)时,TCP_QUICKACK 选项可以设置为0。对处于“连接”状态下的套接字该选项的缺省值是1,首次使用以后内核将把该选项立即复位为1(这是个一次性的选项)。
在某些情形下,发出ACK包则非常有用。ACK包将确认数据块的接收,而且,当下一块被处理时不至于引入延迟。这种数据传输模式对交互过程是相当典型的,因为此类情况下用户的输入时刻无法预测。在Linux系统上这就是缺省的套接字行为。
在上述情况下,客户程序在向服务器发送HTTP请求,而预先就知道请求包很短所以在连接建立之后就应该立即发送,这可谓HTTP的典型工作方式。既然没有必要发送一个纯粹的ACK包,所以设置TCP_QUICKACK为0以提高性能是完全可能的。在服务器方,这两种选项都只能在侦听套接字上设置一次。所有的套接字,也就是被接受呼叫间接创建的套接字则会继承原有套接字的所有选项。
通过TCP_CORK、TCP_DEFER_ACCEPT和 TCP_QUICKACK选项的组合,参与每一HTTP交互的数据包数量将被降低到最小的可接受水平(根据TCP协议的要求和安全方面的考虑)。结果不仅是获得更快的数据传输和请求处理速度而且还使客户/服务器双向延迟实现了最小化。

socket

socket

目 录

  1. socket
    1. socket()
    2. bind()
    3. connect()
    4. listen()
    5. accept()
    6. send() 和recv()
    7. sendto() 和recvfrom()
    8. close() 和shutdown()
    9. getpeername()
    10. gethostname()


socket

[目录]


socket()

我们使用系统调用socket()来获得文件描述符:
#include
#include
int socket(int domain,int type,int protocol);
第一个参数domain设置为“AF_INET”。
第二个参数是套接口的类型:SOCK_STREAM或
SOCK_DGRAM。第三个参数设置为0。
系统调用socket()只返回一个套接口描述符,如果出错,则返回-1。


[目录]


bind()

一旦你有了一个套接口以后,下一步就是把套接口绑定到本地计算机的某一个端口上。但如果你只想使用connect()则无此必要。
下面是系统调用bind()的使用方法:
#include
#include
intbind(int sockfd,struct sockaddr*my_addr,int addrlen);
第一个参数sockfd是由socket()调用返回的套接口文件描述符。
第二个参数my_addr是指向数据结构sockaddr的指针。数据结构sockaddr中包括了关于你的地址、端口和IP地址的信息。
第三个参数addrlen可以设置成sizeof(structsockaddr)。
下面是一个例子:
#include
#include
#include
#defineMYPORT3490
main()
{
int sockfd;
struct sockaddr_inmy_addr;
sockfd=socket(AF_INET,SOCK_STREAM,0);/*do someerror checking!*/
my_addr.sin_family=AF_INET;/*hostbyteorder*/
my_addr.sin_port=htons(MYPORT);/*short,network byte order*/
my_addr.sin_addr.s_addr=inet_addr("132.241.5.10");
bzero(&(my_addr.sin_zero),8);/*zero the rest of the struct*/
/*don't forget your error checking for bind():*/
bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr));
...
如果出错,bind()也返回-1。
如果你使用connect()系统调用,那么你不必知道你使用的端口号。当你调用connect()时,它检查套接口是否已经绑定,如果没有,它将会分配一个空闲的端口。


[目录]


connect()

系统调用connect()的用法如下:
#include
#include
int connect(int sockfd,struct sockaddr* serv_addr,int addrlen);
第一个参数还是套接口文件描述符,它是由系统调用socket()返回的。
第二个参数是serv_addr是指向数据结构sockaddr的指针,其中包括目的端口和IP地址。
第三个参数可以使用sizeof(structsockaddr)而获得。
下面是一个例子:
#include
#include
#include
#defineDEST_IP"132.241.5.10"
#defineDEST_PORT23
main()
{
intsockfd;
structsockaddr_indest_addr;/*will hold the destination addr*/
sockfd=socket(AF_INET,SOCK_STREAM,0);/*do some error checking!*/
dest_addr.sin_family=AF_INET;/*hostbyteorder*/
dest_addr.sin_port=htons(DEST_PORT);/*short,network byte order*/
dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);
bzero(&(dest_addr.sin_zero),8);/*zero the rest of the struct*/
/*don'tforgettoerrorchecktheconnect()!*/
connect(sockfd,(structsockaddr*)&dest_addr,sizeof(struct sockaddr));
...
同样,如果出错,connect()将会返回-1。


[目录]


listen()

如果你希望不连接到远程的主机,也就是说你希望等待一个进入的连接请求,然后再处理它们。这样,你通过首先调用listen(),然后再调用accept()来实现。
系统调用listen()的形式如下:
intl isten(int sockfd,int backlog);
第一个参数是系统调用socket()返回的套接口文件描述符。
第二个参数是进入队列中允许的连接的个数。进入的连接请求在使用系统调用accept()应答之前要在进入队列中等待。这个值是队列中最多可以拥有的请求的个数。大多数系统的缺省设置为20。你可以设置为5或者10。当出错时,listen()将会返回-1值。
当然,在使用系统调用listen()之前,我们需要调用bind()绑定到需要的端口,否则系统内核将会让我们监听一个随机的端口。所以,如果你希望监听一个端口,下面是应该使用的系统调用的顺序:
socket();
bind();
listen();
/*accept()goeshere*/


[目录]


accept()

系统调用accept()比较起来有点复杂。在远程的主机可能试图使用connect()连接你使用
listen()正在监听的端口。但此连接将会在队列中等待,直到使用accept()处理它。调用accept()
之后,将会返回一个全新的套接口文件描述符来处理这个单个的连接。这样,对于同一个连接
来说,你就有了两个文件描述符。原先的一个文件描述符正在监听你指定的端口,新的文件描
述符可以用来调用send()和recv()。
调用的例子如下:
#include
intaccept(intsockfd,void*addr,int*addrlen);
第一个参数是正在监听端口的套接口文件描述符。第二个参数addr是指向本地的数据结构
sockaddr_in的指针。调用connect()中的信息将存储在这里。通过它你可以了解哪个主机在哪个
端口呼叫你。第三个参数同样可以使用sizeof(structsockaddr_in)来获得。
如果出错,accept()也将返回-1。下面是一个简单的例子:
#include
#include
#include
#defineMYPORT3490/*theportuserswillbeconnectingto*/
#defineBACKLOG10/*howmanypendingconnectionsqueuewillhold*/
main()
{
intsockfd,new_fd;/*listenonsock_fd,newconnectiononnew_fd*/
structsockaddr_inmy_addr;/*myaddressinformation*/
structsockaddr_intheir_addr;/*connector'saddressinformation*/
intsin_size;
sockfd=socket(AF_INET,SOCK_STREAM,0);/*dosomeerrorchecking!*/
my_addr.sin_family=AF_INET;/*hostbyteorder*/
my_addr.sin_port=htons(MYPORT);/*short,networkbyteorder*/
my_addr.sin_addr.s_addr=INADDR_ANY;/*auto-fillwithmyIP*/
bzero(&(my_addr.sin_zero),8);/*zerotherestofthestruct*/
/*don'tforgetyourerrorcheckingforthesecalls:*/
bind(sockfd,(structsockaddr*)&my_addr,sizeof(structsockaddr));
listen(sockfd,BACKLOG);
sin_size=sizeof(structsockaddr_in);
new_fd=accept(sockfd,&their_addr,&sin_size);
...
下面,我们将可以使用新创建的套接口文件描述符new_fd来调用send()和recv()。


[目录]


send() 和recv()

系统调用send()的用法如下:
int send(int sockfd,const void* msg,int len,int flags);
第一个参数是你希望给发送数据的套接口文件描述符。它可以是你通过socket()系统调用返回的,也可以是通过accept()系统调用得到的。
第二个参数是指向你希望发送的数据的指针。
第三个参数是数据的字节长度。第四个参数标志设置为0。
下面是一个简单的例子:
char*msg="Beejwashere!";
intlen,bytes_sent;
..
len=strlen(msg);
bytes_sent=send(sockfd,msg,len,0);
...
系统调用send()返回实际发送的字节数,这可能比你实际想要发送的字节数少。如果返回的字节数比要发送的字节数少,你在以后必须发送剩下的数据。当send()出错时,将返回-1。
系统调用recv()的使用方法和send()类似:
int recv(int sockfd,void* buf,int len,unsigned int flags);
第一个参数是要读取的套接口文件描述符。
第二个参数是保存读入信息的地址。
第三个参数是缓冲区的最大长度。第四个参数设置为0。
系统调用recv()返回实际读取到缓冲区的字节数,如果出错则返回-1。
这样使用上面的系统调用,你可以通过数据流套接口来发送和接受信息。


[目录]


sendto() 和recvfrom()

因为数据报套接口并不连接到远程的主机上,所以在发送数据包之前,我们必须首先给出目的地址,请看:
int sendto(int sockfd,const void* msg,int len,unsigned int flags,
conststruct sockaddr*to,inttolen);
除了两个参数以外,其他的参数和系统调用send()时相同。
参数to是指向包含目的IP地址和端口号的数据结构sockaddr的指针。
参数tolen可以设置为sizeof(structsockaddr)。
系统调用sendto()返回实际发送的字节数,如果出错则返回-1。
系统调用recvfrom()的使用方法也和recv()的十分近似:
int recvfrom(int sockfd,void* buf,int len,unsigned int flags
struct sockaddr* from,int* fromlen);
参数from是指向本地计算机中包含源IP地址和端口号的数据结构sockaddr的指针。
参数fromlen设置为sizeof(struct sockaddr)。
系统调用recvfrom()返回接收到的字节数,如果出错则返回-1。


[目录]


close() 和shutdown()

你可以使用close()调用关闭连接的套接口文件描述符:
close(sockfd);
这样就不能再对此套接口做任何的读写操作了。
使用系统调用shutdown(),可有更多的控制权。它允许你在某一个方向切断通信,或者切断双方的通信:
int shutdown(int sockfd,int how);
第一个参数是你希望切断通信的套接口文件描述符。第二个参数how值如下:
0—Furtherreceivesaredisallowed
1—Furthersendsaredisallowed
2—Furthersendsandreceivesaredisallowed(likeclose())
shutdown()如果成功则返回0,如果失败则返回-1。


[目录]


getpeername()

这个系统的调用十分简单。它将告诉你是谁在连接的另一端:
#include
int getpeername(int sockfd,struct sockaddr* addr,int* addrlen);
第一个参数是连接的数据流套接口文件描述符。
第二个参数是指向包含另一端的信息的数据结构sockaddr的指针。
第三个参数可以设置为sizeof(structsockaddr)。
如果出错,系统调用将返回-1。
一旦你获得了它们的地址,你可以使用inet_ntoa()或者gethostbyaddr()来得到更多的信息。


[目录]


gethostname()

系统调用gethostname()比系统调用getpeername()还简单。它返回程序正在运行的计算机的名字。系统调用gethostbyname()可以使用这个名字来决定你的机器的IP地址。
下面是一个例子:
#include
int gethostname(char*hostname,size_tsize);
如果成功,gethostname将返回0。如果失败,它将返回-1。


[目录]

2007年7月19日星期四

网络编程:Java数据报之失序和丢包




习惯了TCP编程,认为UDP可以包办这些问题是错误的。一个UDP应用程序要承担可靠性方面的全部工作,包括报文的丢失、重复、时延、乱序以及连接失效等问题。

  通常我们在可靠性好,传输时延小的局域网上开发测试,一些问题不容易暴露,但在大型互联网上却会出现错误。

  UDP协议把递送的可靠性责任推到了上层即应用层,下面简单编写了几个类来专门处理两个问题:乱序和丢包。

  四个类:DataPacket
类,PacketHeader类,PacketBody类 ,DataEntry类,位于同一个文件DataPacket .java中。

  DataPacket
类相当于一个门面模式,提供给外部使用,通信数据也在这个类中处理。

package com.skysoft.pcks;

import java.io.*;
import java.net.*;
import java.util.*;

public class DataPacket {
InputStream
is;
OutputStream
os;
PacketHeader
header;
PacketBody
body;
ArrayList
al;
public static final int DataSwapSize = 64532;

/**
* 在接收数据报使用
*/
public DataPacket() {
header
= new PacketHeader();
body
= new PacketBody();
al
= new ArrayList();
}
/**
* 在发送数据报时使用,它调用报文分割操作.
* @param file String 硬盘文件
*/
public DataPacket(String file) {
this();
try {
is
= new FileInputStream(file);
header.CalcHeaderInfo(is.available());
this.madeBody();
is.close();
//this.Gereratedata();
}
catch (FileNotFoundException ex) {
ex.printStackTrace();
}
catch (IOException ex1) {
ex1.printStackTrace();
}
}
/**
* 在发送数据报时使用,它调用报文分割操作.
* @param url URL url地址
*/
public DataPacket(URL url) {
this();
try {
//is = url.openStream();
URLConnection conn=url.openConnection();
is=conn.getInputStream();
int total=conn.getContentLength();
header.CalcHeaderInfo(total);
this.madeBody();
//System.out.println(total+":"+total);
is.close();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
/**
* 为发送构造分组,使用PackageHeader处理了报头格式,并为分组编序号.
*/
private void madeBody() {
al.clear();
byte[] buffer;
DataEntry
de;
for (int i = 0; i < header.fragmentcounter; i++) {
try {
ByteArrayOutputStream
bos = new ByteArrayOutputStream();
//is.skip(i * body.BODY_BUFFER_SIZE);
header.ArrageSort(i);
de
= new DataEntry(PacketBody.BODY_BUFFER_SIZE);
de.setSn(i);
de.setStreamsize(header.getStreamsize());
de.setFragmentcounter(header.getFragmentcounter());
if (header.isWTailFragment(i)) {
buffer
= new byte[header.getMinfragment()];
is.read(buffer,
0, buffer.length);
header.setActByteSize(header.getMinfragment());
de.setActByteSize(header.getMinfragment());
}
else {
buffer
= new byte[body.BODY_BUFFER_SIZE];
is.read(buffer,
0, buffer.length);
}
//System.out.println("length-------"+i+" "+body.getBody().length+" "+header.getMinfragment());
body.setBody(buffer);
//System.out.println("length:" + i + " " + header.toString());
bos.write(header.getByte(), 0, header.HEADER_BUFFER_SIZE);
bos.write(body.getBody(),
0, body.getBody().length);
de.setBytes(bos.toByteArray());
al.add(de);
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}
/**
* 为发送构造分组,没有考虑报头格式,也没有为分组编序号.
*/
private void madeBody1() {
al.clear();
for (int i = 0; i < header.fragmentcounter; i++) {
try {
if (header.isWTailFragment(i))
is.read(body.getBody(),
i * body.BODY_BUFFER_SIZE,
header.getMinfragment());
else
is.read(body.getBody(), i * body.BODY_BUFFER_SIZE,
body.BODY_BUFFER_SIZE);
ByteArrayOutputStream
bos = new ByteArrayOutputStream();
bos.write(header.getByte(),
0, header.HEADER_BUFFER_SIZE);
bos.write(body.getBody(),
header.HEADER_BUFFER_SIZE,
body.getBody().length);
al.add(bos);
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}
/**
* 在接收到报文后,对此报文执行组装,并处理报文丢失和乱序情况.
* @param b1 byte[]
*/
public void Add(byte[] b1) {
byte[] buffer = (byte[]) b1.clone();
handlerText(buffer);
DataEntry
de = new DataEntry(buffer, header.getActByteSize());
de.setSn(header.getSn());
de.setStreamsize(header.getStreamsize());
de.setFragmentcounter(header.getFragmentcounter());
al.add(de);
}
private void handlerText(byte[] buffer) {
ByteArrayOutputStream
baos = new ByteArrayOutputStream();
baos.write(buffer,
0, header.HEADER_BUFFER_SIZE);
byte[] b=new byte[header.HEADER_BUFFER_SIZE];
System.arraycopy(buffer,
0,b,0,b.length);
ByteArrayInputStream
bais = new ByteArrayInputStream(baos.toByteArray());
InputStreamReader
isr = new InputStreamReader(bais);
BufferedReader
br = new BufferedReader(isr);
try {
header
= new PacketHeader(br.readLine());
}
catch (Exception ex) {
ex.printStackTrace();
}
}

private String calFileSize(int size) {
return size / 1024 + "K";
}

public ArrayList getDataPackets() {
return al;
}
/**
* 是否接收完毕,通过序号是否等于最大段数来判断,这也许有问题,比如,正好是最后一个段丢失了,这样
* 这个包整个就丢失了.
* @return
*/
public boolean isFull() {
return this.header.getSn() == this.header.getFragmentcounter() - 1 ? true : false;
}
/**
* 判断是否只有一个段.
* @return
*/
public boolean isZero() {
return this.header.getSn() == 0 ? true : false;
}
/**
* 该函数执行报文组装,不考虑丢失的报文.
* @return
*/
private ByteArrayOutputStream fetchDataPackets() {
ByteArrayOutputStream
bos = new ByteArrayOutputStream();
byte[] buffer = null;
DataEntry
de;
for (int i = 0; i < al.size(); i++) {
try {
de
= this.getSnData(i);
buffer
= de.getByte();
if (header.getStreamsize() == de.getStreamsize()) {
bos.write(de.getByte(),
header.HEADER_BUFFER_SIZE, de.getActByteSize());
System.out.println(de.toString()
+ " -- fetchDataPackets");
}
}
catch (Exception ex) {
ex.printStackTrace();
}
}
return bos;
}

/**
* 该函数执行报文组装,对于丢失的报文,写入空报文.
* @return ByteArrayOutputStream
*/
private ByteArrayOutputStream fetchDataPackets_sn() {
ByteArrayOutputStream
bos = new ByteArrayOutputStream();
byte[] buffer;
DataEntry
de;
for (int i = 0; i < header.getFragmentcounter(); i++) {
try {
de
= this.getSnData(i);
if (de == null) {
de
= seachDeData(i);
}
buffer
= de.getByte();
//System.out.println(de.getSn() + ":" + i);
//handlerText(buffer);
//bos.write(buffer, header.HEADER_BUFFER_SIZE,
// buffer.length - header.HEADER_BUFFER_SIZE);
if (header.getStreamsize() == de.getStreamsize()) {
bos.write(de.getByte(),
header.HEADER_BUFFER_SIZE,
de.getActByteSize());
//System.out.println(de.toString());
}
}
catch (Exception ex) {
ex.printStackTrace();
}
}
return bos;
}

/**
* 对缓冲的数据包进行排序处理,即按顺序提取同一帧的数据,如果没有找到该序号的帧,则返回空值.
* @param sn int 要找的帧序号.
* @return DataEntry
*/
private DataEntry getSnData(int sn) {
DataEntry
de = null;
for (int i = 0; i < al.size(); i++) {
de
= (DataEntry) al.get(i);
if (header.getStreamsize() == de.getStreamsize()) {
if (sn == de.getSn())
break;
else
de = null;
}
}
return de;
}

/**
* 按序号开始向前或者是向后寻找最近的帧片段,日后可以增加请求重发功能,通过开一个通信连接.
* @param sn int
* @return DataEntry
*/
private DataEntry seachDeData(int sn) {
DataEntry
de = null;
int initvalue, minvalue = 10000;
DataEntry
back, fore = null;
for (int i = 0; i < al.size(); i++) {
de
= (DataEntry) al.get(i);
if (header.getStreamsize() == de.getStreamsize()) {
initvalue
= Math.abs(de.getSn() - sn);
if (de.getFragmentcounter() != de.getSn() && initvalue < minvalue) {
minvalue
= initvalue;
fore
= de;
}
}
}
return fore;
}

/**
* 除去最后一帧外,随机抽取一帧.
* @return DataEntry
*/
private DataEntry seachDeData() {
DataEntry
de = null;
for (int i = 0; i < al.size(); i++) {
de
= (DataEntry) al.get(i);
System.out.println(
"sky ::::" + de.getFragmentcounter() + ":" + de.getSn() +
":" + i);
if (header.getStreamsize() == de.getStreamsize()) {
if (de.getFragmentcounter() != de.getSn()) {
break;
}
}
}
return de;
}
/**
* 生成组装完的结果数据.因为用图像来做测试,所以令其返回图像.
* @return Image
*/
public java.awt.Image Gereratedata() {
ByteArrayInputStream
bis;
java.awt.image.BufferedImage
bimage = null;
try {
byte[] b = fetchDataPackets_sn().toByteArray();
//fetchDataPackets_old1()
bis = new ByteArrayInputStream(b);
bimage
= javax.imageio.ImageIO.read(bis);

}
catch (Exception ex1) {
ex1.printStackTrace();
}
return bimage;
}

public static void main(String args[]) {
DataPacket
dp = new DataPacket("e:\\nature\\14.jpg");
}
}
/**
* 数据实体,充当临时处理场所.
* @author Administrator
*
*/
class DataEntry {
byte[] bytes;
int fragmentcounter, sn, actbytesize;
long streamsize;
int minfragment;

public DataEntry() {

}

public DataEntry(int size) {
this.actbytesize = size;
}

public DataEntry(byte[] b, int i) {
this.bytes = b;
this.actbytesize = i;
}

public byte[] getByte() {
return this.bytes;
}

public void setBytes(byte[] b) {
this.bytes = b;
}

public void setStreamsize(long size) {
this.streamsize = size;
}

public long getStreamsize() {
return this.streamsize;
}

public int getMinfragment() {
return minfragment;
}

public synchronized void setSn(int i) {
this.sn = i;
}

public synchronized int getSn() {
return sn;
}

public synchronized int getFragmentcounter() {
return fragmentcounter;
}

public synchronized void setFragmentcounter(int c) {
this.fragmentcounter = c;
}

public void setActByteSize(int size) {
actbytesize
= size;
}

public int getActByteSize() {
return actbytesize;
}

public String toString() {
return this.streamsize + "::" + this.fragmentcounter + "::" + this.sn +
"::" + this.actbytesize + " recv DataEntry";
}
}
/**
* 报头,处理报头格式
* @author Administrator
*
*/
class PacketHeader implements Serializable{
public static final int HEADER_BUFFER_SIZE = 1024;
int fragmentcounter, sn;
int actbytesize = PacketBody.BODY_BUFFER_SIZE;
byte[] header; //= new byte[HEADER_BUFFER_SIZE];
long streamsize;
int minfragment;

public PacketHeader() {

}

public PacketHeader(long l) {
this.setStreamsize(l);

}

public PacketHeader(String s) {
String[]
tm = s.split("::");
this.setActByteSize(Integer.parseInt(tm[3]));
this.setSn(Integer.parseInt(tm[2]));
this.setFragmentcounter(Integer.parseInt(tm[1]));
this.setStreamsize(Long.parseLong(tm[0]));
}

/**
* 根据文件的段的顺序生成数据头.
* @param sn 文件序列
*/
public void ArrageSort(int sn) {
this.setSn(sn);
this.setByte();
}

public void CalcHeaderInfo(long l) {
this.setStreamsize(l);
CalcHeaderInfo();
}
/**
* 计算流要被分成的片段数量,并得出最小片段余量.
*/
public void CalcHeaderInfo() {
fragmentcounter
= Math.round( (float) streamsize /
PacketBody.BODY_BUFFER_SIZE);
float critical = (float) streamsize / PacketBody.BODY_BUFFER_SIZE;
if (critical - fragmentcounter < 0.5 && critical - fragmentcounter > 0)
fragmentcounter++;
minfragment
= (int) (streamsize % PacketBody.BODY_BUFFER_SIZE);
}

public byte[] getHeader() {
Long
it = new Long(this.streamsize);
return new byte[] {it.byteValue()};
}

public byte[] getByte() {
return header; //this.toString().getBytes();
}
/**
* 生成报头字节,首先取得数据包头 流尺寸::段片数::段顺序::段实际尺寸 的字节形式,
* 然后加入回车换行符号,对于1024字节中剩余的部分一律写入元素为0的字节数组.
*/
public void setByte() {
ByteArrayOutputStream
bos = new ByteArrayOutputStream();
byte[] buffer = this.toByte();
try {
bos.write(buffer);
bos.write(
"\r\n".getBytes());
bos.write(
new byte[PacketHeader.HEADER_BUFFER_SIZE - buffer.length], 0,
PacketHeader.HEADER_BUFFER_SIZE
- buffer.length);
header
= bos.toByteArray();
}
catch (IOException ex) {
ex.printStackTrace();
}
}

public void setStreamsize(long size) {
this.streamsize = size;
}

public long getStreamsize() {
return this.streamsize;
}

public int getMinfragment() {
return minfragment;
}

public synchronized void setSn(int i) {
this.sn = i;
}

public int getSn() {
return sn;
}

public int getFragmentcounter() {
return fragmentcounter;
}

public synchronized void setFragmentcounter(int c) {
this.fragmentcounter = c;
}

public void setActByteSize(int size) {
actbytesize
= size;
setByte();
}

public int getActByteSize() {
return actbytesize;
}
/**
* 数据包头的格式为:流尺寸::段片数::段顺序::段实际尺寸
* 报头字节长度是可变化的,比如,可以加入流的具体信息如:流所属文件的名称,文件类型以及一些其他信息.
* @return String
*/
public String toString() {
return streamsize + "::" + this.fragmentcounter + "::" + this.getSn() +
"::" + this.getActByteSize();
}

public byte[] toByte() {
return this.toString().getBytes();
}
/**
* 是否为尾段
* @param i int
* @return boolean
*/
public boolean isWTailFragment(int i) {
return (i == fragmentcounter - 1) ? true : false;
}

}
/**
* 用户数据区
* @author Administrator
*
*/
class PacketBody implements Serializable{
public static final int BODY_BUFFER_SIZE = 63508; //65508
byte[] body;

public PacketBody() {
}

public void setBody(byte[] b) {
this.body = b;
}

public byte[] getBody() {
return body;
}
}

  这个数据处理类,将在接下来使用