Socket网络编程 - xgc94418297的日志 - 网易博客

来源:互联网 发布:网络无internet 编辑:程序博客网 时间:2024/06/02 03:15

网络编程 2009-04-28 19:43:58

  一、什么是Socket
     Socket(套接字)是BSD提供的网络应用编程界面(API),Socket接口定义了许多函数和例程,程序员可以用它们来开发TCP/IP网络上的应用程序.现在它已是网络编程中的标准.
     Socket是一种特殊的进程间通信方式,不同及其上的进程都可以使用这种方式进行通信,网络中的数据传输是一种I/O操作.
     Socket也是一种文件描述符,它代表了一个通信管道的一个端点,read、write、close操作可应用于Socket描述符.在Socket文件描述符上,可以完成建立连接、数据传输等操作.
     常用的Socket类型有两种:流式Socket SOCK_STREAM,提供面向连接的Socket;
                             数据报式Socket SOCK_DGRAM,提供面向无连接的Socket.
二、设置IP地址和端口号
    字节序转换:字节序是指一个字节在存储器中是按高字节在前还是低字节在前存放,分为大端格式和小端格式,异构计算机之间的通信,需要转换自己的字节序为网络字节序.
    字节序转换函数:(包含头文件<arpa/inet.h>)
uint32_t  htonl()
uint16_t  htons()
以上返回网络字节序;
uint32_t  ntohl()
uint16_t  ntohs()
以上返回主机字节序;
 
     以下函数包含头文件<netinet/in.h>   
通用套接口地址结构体:
struct   sockaddr
{
           sa_family_t    sa_family; /*地址族,unsigned short */ /* address family, AF_xxx */
           char    sa_data[14]; /*地址数据*/ /* 14 bytes of protocol address */
};
在使用其他套接口地址时要强制转换成通用套接口地址:(struct sockaddr *) &servAddr;
 
IPv4套接字地址结构体:
struct   sockaddr_in
{
          sa_family_t      sin_family; /*协议类型,unsigned short*/ /* Address family  */
          __be16    sin_port;  /*端口号,unsigned short,(__u16)*/ /* Port number   */
          struct   in_addr   sin_addr;  /*IP地址,只有一个成员__be32 s_addr,(unsigned int)*/ /* Internet address  */
          unsigned  char  __pad[8];  /*为0*/ /* Pad to size of `struct sockaddr'. */
};
IP地址结构体:
struct  in_addr
{
           __be32   s_addr;  /*unsigned int*/
};
 
        struct  in_addr (IP地址结构体),以一个32位的无符号数来表示,通常需要用到点分十进制数串与它之间的转换.
使用inet_pton()和inet_ntop()函数来处理网络字节和主机字节之间的转换,函数原型如下:
int inet_pton(int family,const char *strptr,void *addrptr);
如果使用IPv4协议,第一个参数:family=AF_INET;
                 第二个参数:strptr为指向字符型的地址(ASCII地址的首地址,ddd.ddd.ddd.ddd格式),
                 函数将该地址转化为 struct in_addr 的结构体,并复制到第三个参数所指 *addrptr中.
                 成功返回1,否则返回0;/*(如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,
                                                                     如果参数af指定的地址族和src格式不对,函数将返回0.)*/
 
 如果使用IPv6协议,第一个参数:family=AF_INET6
                  第二个参数:strptr为指向IPV6的地址,函数将该地址转换为 in6_addr 的结构体,并复制在*addrptr中 ;
 
const char *inet_ntop(int family,const void *addrptr,char *strptr,size_t len);
这个函数转换网络二进制结构体到ASCII类型的地址(addrptr->strptr),函数的作用相反,参数的作用和上面的相同,多了一个参数len,它是所指向缓冲区addrptr的大小,避免溢出,如果缓冲区太小无法存储地址值,则返回一个空指针,并将errno置为ENOSPC.
 
有一个宏定义,定义了IP地址的长度为16,可以直接拿来用: #define INET_ADDRSTRLEN 16
 
三、建立Socket
    以下函数包含 <sys/socket.h>
   创建套接字是进行任何网络通信时必须进行的第一步,创建一个用于网络通信的I/O描述符(套接字),相当于在对文件读写前先用open获取文件描述符.
   为了建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄.
Socket函数原型为:int socket(int family,int type,int protocol);
     第一个参数:family,是协议族,常用的是AF_INET,代表IPv4协议;AF_INET6代表的是IPv6协议;
   第二个参数:type,套接口类型,流式Socket SOCK_STREAM 或 数据报式Socket SOCK_DGRAM;
   第三个参数:protocol,协议类型,一般为O就可以了;
调用Socket函数,返回一个int型的Socket描述符,Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口;
调用Socket函数,Socket执行体将建立一个Socket,实际上"建立一个Socket"意味着为一个Socket数据结构分配存储空间;
Socket执行体为你管理描述符表;
 
四、作为服务器
    作为服务器需要具备三个条件:
1.具备一个可以确知的地址,以便别人能找到我;
2.让操作系统知道你是一个服务器,而不是一个客户端;
3.等待连接的到来;
1.使用一个确知的端口来接收客户端的连接,需要时用bind函数将一个地址绑定到套接字;
bind函数原型:int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
第一个参数:sockfd 套接口描述字;
第二个参数:myaddr 指向特定于协议的地址结构体指针;
第三个参数:addrlen 该地址结构体的长度
返回值:0,成功;其他,失败; /*功能:绑定端口,将开始初始化的值绑定到sockfd*/
 
2.让套接字成为被动的,使用listen函数可以将套接口由主动修改为被动.

listen函数原型:int listen(int sockfd, int backlog);

第一个参数:sockfd socket套接口描述字

第二个参数:backlog 连接队列的长度

返回值:0,成功;其他,失败;/*功能:sockfd 用于监听的套接口, backlog是监听到的套接口队列长度*/


3.等待连接的到来,使用accept函数从连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(这个函数是阻塞的).

accept函数原型:int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);

第一个参数:sockfd  socket套接口描述字

第二个参数:cliaddr 客户端地址

第三个参数:addrlen 客户端地址结构体长度

返回值:已连接的套接口(代表当前连接). /*功能:用于监听的套接口sockfd, 接收到客户套接口地址的地址结构,指向接受套接口地址缓存最大长度的指针的addrlen. accept()返回一个新的套接口描述符,服务器的生产的新的这个套接口就对应一个客户*/                  

五、做为客户端

客户端要知道服务器的IP地址以及端口号,需要主动跟服务器建立连接,对于TCP协议,连接建立后才可以开始传输数据,使用connect函数建立连接.

connect函数原型:int connect(int sockfd,const struct sockaddr *addr,socklen_t len);

第一个参数:sockfd  socket套接口描述字.

第二个参数:addr    服务器地址.

第三个参数:addrlen 服务器地址结构体长度.

返回值:0,成功;其他,错误.

connect函数建立连接后不会产生新的套接口

六、传输数据

    当连接建立后,通信的两端便具备两个套接口,可以用read,write函数从这个通信管道读取后写入数据.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不匹配时,应该对这种情况进行处理.  /*功能:将本地的消息送给套接口sockfd */

例子:
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值./*功能:将套接口sockfd 发送过来的东西读到buf 中*/

      使用close函数关闭套接字,关闭一个代表已建立连接的套接字将导致另一端接收到一个0长度的数据包;

      服务器关闭socket创建的套接字,导致服务器无法继续接收新的连接,不会影响已经建立的连接.

      关闭accept返回的套接字,将导致它所代表的连接被关闭,但不会影响服务器的监听.

      客户端用close就是关闭连接.

 

可以使用shutdown()函数来关闭socket,该函数允许只停止在某个方向上的数据传输,而另一个方向上的数据传输继续进行.

例如可以关闭某socket的写操作而允许继续在该socket上继续接收数据,直至读入所有数据.

shutdown()函数原型:int shutdown(int sockfd,int how);

第一个参数:sockfd 需要关闭的socket描述符.

第二个参数:how 允许shutdown()函数选择以下几种方式

                   0->不允许继续接收数据

                   1->不允许继续发送数据

                   2->不允许继续接收和发送数据

                   均为允许调用close()函数

返回值:shutdown在操作成功时返回0,在出现错误时返回-1并置相应errno.

 例子:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

//=============================================================

// 语法格式: void main(void)

// 实现功能: 主函数,建立一个TCP Echo Server

// 入口参数:

// 出口参数:

//=============================================================
int main(int argc,char *argv[])

{

             char recvbuf[2048];                // 接收缓冲区

      int sockfd;                        // 套接字

      struct sockaddr_in servAddr;       // 服务器地址结构体

      unsigned short port = 8000;        // 监听端口

      if(argc > 1)                       // 由参数接收端口

      {

            port = atoi(argv[1]);

      }

      printf("TCP Server Started at port %d!\n", port);

/*

unsigned short port = 8000;

定义端口号为8000

int sockfd;

sockfd = socket(AF_INET, SOCK_STREAM, 0);

socket函数创建一个套接字,使用IPv4TCP协议

要加上一个错误判断,如果socket返回值小于0,就是错误的

*/

      sockfd = socket(AF_INET,SOCK_STREAM,0);    // 创建TCP套接字     

      

      if(sockfd < 0)

     {

            perror("Invalid socket");

            exit(1);

     }

     /*  

     // 初始化服务器地址

     //bzero(&servAddr, sizeof(servAddr));

     //定义了服务器地址结构体,并初始化,bzero相当于memset,将结构体内容清0

     */

     bzero(&servAddr,sizeof(servAddr)); 

     /*

      //给服务器地址赋值:

      //servAddr.sin_family = AF_INET;                //IPv4

     //servAddr.sin_port = htons(port);              //端口号,注意要把port转换为(16位)网络字节序

     //servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址,用INADDR_ANY指定为任意IP,转换成(32位)网络字节序

      */      

      servAddr.sin_family = AF_INET;

     servAddr.sin_port = htons(port);

     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

 

     printf("Binding server to port %d\n", port);

/*

bind(sockfd, (struct sockaddr*)&servAddr, sizeof(struct sockaddr))

绑定地址,要把服务器地址强制转换成sockaddr的结构体,同样加错误判断

*/

     if(bind(sockfd, (struct sockaddr*)&servAddr, sizeof(struct sockaddr)) != 0)

     {

         close(sockfd);

         perror("binding err!");

         exit(1);

 }

/*

listen(sockfd, 1)

开始监听,队列大小为1,只允许一个客户端访问,但实际在linux下不起作用\

*/

     if(listen(sockfd, 1) != 0)

     {

        close(sockfd);

        perror("listen err!");

        exit(1);

     }

     printf("waiting client...\n");

     while(1)

     {

/*

char cliIP[INET_ADDRSTRLEN];

定义一个存放客户端IP地址的数组,大小为16

*/

        char cliIP[INET_ADDRSTRLEN];    // 用于保存客户端IP地址

 

        size_t recvLen;

/*

struct sockaddr_in cliAddr;

定义了客户端地址结构体

*/

        struct sockaddr_in cliAddr;     // 用于保存客户端地址

        size_t cliAddrLen = sizeof(cliAddr);

                                   // 必须初始化!!!

/*

int connfd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliAddrLen);

客户端连接到服务器,获得一个已经建立的连接connfd,这个就是当前连接,对它进行读写操作

*/       

        int connfd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliAddrLen);

                                   // 获得一个已经建立的连接

        if(connfd < 0)

        {

           close(sockfd);

           perror("accept err!");

           exit(1);

        }

/*

inet_ntop(AF_INET, &cliAddr.sin_addr.s_addr, cliIP, INET_ADDRSTRLEN);

将客户端的IP地址转换成字符串,存放在cliIP中,INET_ADDSTRLEN指定大小为16

*/

        inet_ntop(AF_INET, &cliAddr.sin_addr.s_addr, cliIP, INET_ADDRSTRLEN);

 

        printf("client ip = %s\n", cliIP);

        while((recvLen = read(connfd, recvbuf, 2048)) > 0)

        {

           write(connfd, recvbuf, recvLen);

        }

        close(connfd);

        printf("client closed!\n");

    }

    close(sockfd);

    return 0;

}

分析:

服务器:

unsigned short port = 8000;

定义端口号为8000

int sockfd;

sockfd = socket(AF_INET, SOCK_STREAM, 0);

socket函数创建一个套接字,使用IPv4TCP协议

要加上一个错误判断,如果socket返回值小于0,就是错误的

 

struct sockaddr_in servAddr;

bzero(&servAddr, sizeof(servAddr));

定义了服务器地址结构体,并初始化,bzero相当于memset,将结构体内容清0

 

给服务器地址赋值:

servAddr.sin_family = AF_INET;                //IPv4

servAddr.sin_port = htons(port);              //端口号,注意要把port转换为(16位)网络字节序

servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址,用INADDR_ANY指定为任意IP,转换成(32位)网络字节序

 

bind(sockfd, (struct sockaddr*)&servAddr, sizeof(struct sockaddr))

绑定地址,要把服务器地址强制转换成sockaddr的结构体,同样加错误判断

 

listen(sockfd, 1)

开始监听,队列大小为1,只允许一个客户端访问,但实际在linux下不起作用

 

客户端:

char cliIP[INET_ADDRSTRLEN];

定义一个存放客户端IP地址的数组,大小为16

 

struct sockaddr_in cliAddr;

定义了客户端地址结构体

 

int connfd

connfd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliAddrLen);

客户端连接到服务器,获得一个已经建立的连接connfd,这个就是当前连接,对它进行读写操作

 

inet_ntop(AF_INET, &cliAddr.sin_addr.s_addr, cliIP, INET_ADDRSTRLEN);

将客户端的IP地址转换成字符串,存放在cliIP中,INET_ADDSTRLEN指定大小为16

 

到这为止,连接已经建立完成,运行程序,用telnet登录服务器,输入服务器IP和端口号,连接成功,

就可以用readwrite,读写connfd,实现通信。