socket编程

一、socket地址的数据类型及相关函数

    socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6、UNIX Domain Socket。然而各种网络协议的地址格式并不相同,如下图所示:

   

    IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和 32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个 结构体的长度(并不是所有UNIX的实现都有长度地址,Linux就没有),后16位表示地址类型。IPv4、IPv6和UNIX Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知 道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:

struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

   sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换。

    字符串转in_addr的函数: 

#include <arpa/inet.h>

int inet_aton(const char *strptr, struct in_addr * addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);

    in_addr转字符串的函数:

char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

    其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

二、基于TCP协议的网络程序

    

    1、TCP协议通讯流程

    

    建立连接的过程:

   

    服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调 用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从 connect()返回,同时应答一个ACK端,服务器收到后从accept()返回。

    数据传输的过程:

    建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答 的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这是客户端调用 write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务 器调用write()将处理结果发给客户端,再次调用read阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

    关闭连接的过程:

   

    如果客户端没有更多请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端 关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用 shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

    

    2、TCP网络程序

    server.c从客户端读取字符,然后将每个字符转换为大写返回给客户端。

/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
        struct sockaddr_in servaddr, cliaddr;
        socklen_t cliaddr_len;
        int listenfd, connfd;
        char buf[MAXLINE];
        char str[INET_ADDRSTRLEN];
        int i, n;
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(SERV_PORT);
    
        bind(listenfd, (struct sockaddr *)&servaddr, 
sizeof(servaddr));
        listen(listenfd, 20);
        printf("Accepting connections ...\n");
        while (1) {
                cliaddr_len = sizeof(cliaddr);
                connfd = accept(listenfd, 
                                (struct sockaddr *)&cliaddr, 
&cliaddr_len);
          
                n = read(connfd, buf, MAXLINE);
                printf("received from %s at PORT %d\n",
                       inet_ntop(AF_INET, &cliaddr.sin_addr, str, 
sizeof(str)),
                       ntohs(cliaddr.sin_port));
    
                for (i = 0; i < n; i++)
                        buf[i] = toupper(buf[i]);
                write(connfd, buf, n);
                close(connfd);
        }
}

    程序中用到的socket API 这些函数都在sys/socket.h中

int socket(int family, int  type, int protocol);

    socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网 络上收发数据,如果socket调用出错则返回-1.对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。 protocol参数指定为0即可。

int bind(int sockfd, const struct sockaddr &myaddr, socklen_t addrlen);

    服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败则返回-1。

    

    bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的 地址和端口号。struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接收多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定 结构体的长度。我们的程序中对myaddr参数是这样初始化的:

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

    首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡, 每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到我们与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为 SERV_PORT,这里定义为8000。

int listen(int sockfd, int backlog);

   典型的服务器程序可以同时服务与多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服 务器来不及处理,尚未accept()的客户端即处于连接等待状态,如果接受到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

    三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端 连接上来。cliaddr是一个传出参数,accept返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value- result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出的问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用 者提供的缓冲区)。如果给cliaddr参数 传NULL,表示不关心客户端的地址。

    我们的服务器程序结构是这样的:

while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE);
    ...
    close(connfd);
}

    整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。 accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间及通过 这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。 accept()成功返回一个文件描述符,出错则返回-1。

    client.c从命令行参数中获取一个字符串发送给服务端,然后接受服务端返回的字符串然后显示出来:

/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
        struct sockaddr_in servaddr;
        char buf[MAXLINE];
        int sockfd, n;
        char *str;
    
        if (argc != 2) {
                fputs("usage: ./client message\n", stderr);
                exit(1);
        }
        str = argv[1];
    
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
        servaddr.sin_port = htons(SERV_PORT);
    
        connect(sockfd, (struct sockaddr *)&servaddr, 
sizeof(servaddr));
        write(sockfd, str, strlen(str));
        n = read(sockfd, buf, MAXLINE);
        printf("Response from server:\n");
        write(STDOUT_FILENO, buf, n);
        close(sockfd);
        return 0;
}

    由于客户端不需要固定的端口因此不用调用bind(),客户端的端口号由内核自动分配。(服务端不是必须要调用bind(),但是如果不调用bind()内核会自动给服务端分配监听端口,每次启动服务端时端口号都不一样客户端想连接服务端就会比较麻烦)

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

    客户端需要调用connect()连接服务器,conenct和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1.

    然后编译先运行server再运行client。

posted @ 2016-04-14 13:30  orlion  阅读(560)  评论(0编辑  收藏  举报