关于SOCKET编程

来源:互联网 发布:warframe淘宝领冰队 编辑:程序博客网 时间:2024/05/16 14:54

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

大小端的转换可以调用下面这些函数来进行:

#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);



socket address的数据结构如下所示:

<span style="white-space:pre"></span>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和UnixDomain 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;bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

字符串转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转字符串的函数:其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

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


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()则连接处于半关闭状态,仍可接收对方发来的数据。


socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块:

#include<stdio.h>#include <stdlib.h>#include <errno.h>#include <sys/socket.h>void perr_exit(const char *s){    perror(s);    exit(1);}int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr){    int n;    again:    if ( (n = accept(fd, sa, salenptr)) < 0 ) {        if ((errno == ECONNABORTED) || (errno == EINTR))        goto again;        else        perr_exit("accept error");    }    return n;}void Bind(int fd, const struct sockaddr *sa, socklen_t salen){    if (bind(fd, sa, salen) < 0)    perr_exit("bind error");}void Connect(int fd, const struct sockaddr *sa, socklen_t salen){    if (connect(fd, sa, salen) < 0)    perr_exit("connect error");}void Listen(int fd, int backlog){    if (listen(fd, backlog) < 0)    perr_exit("listen error");}int Socket(int family, int type, int protocol){    int n;    if ( (n = socket(family, type, protocol)) < 0 )    perr_exit("socket error");    return n;}ssize_t Read(int fd, void *ptr, size_t nbytes){    ssize_t n;    again:    if ( (n = read(fd, ptr, nbytes)) == -1 ) {        if (errno == EINTR)        goto again;        else        return -1;    }    return n;}mZssize_t Write(int fd, const void *ptr, size_t nbytes){    ssize_t n;    again:    if ( (n = write(fd, ptr, nbytes)) == -1 ) {        if (errno == EINTR)        goto again;        else        return -1;    }    return n;}void Close(int fd){    if (close(fd) == -1)    perr_exit("close error");}size_t Readn(int fd, void *vptr, size_t n){    size_t nleft;    ssize_t nread;    char    *ptr;    ptr = vptr;    nleft = n;    while (nleft > 0) {        if ( (nread = read(fd, ptr, nleft)) < 0 ) {            if (errno == EINTR)            nread = 0;            else            return -1;        } else if (nread == 0)        break;        nleft -= nread;        ptr += nread;    }    return n - nleft;}ssize_t Writen(int fd, const void *vptr, size_t n){    size_t nleft;    ssize_t nwritten;    const char *ptr;    ptr = vptr;    nleft = n;    while (nleft > 0) {        if ( (nwritten = write(fd, ptr, nleft)) <= 0 ) {            if (nwritten < 0 && errno == EINTR)                nwritten = 0;            else                 return -1;        }        nleft -= nwritten;        ptr += nwritten;    }    return n;}
如果应用层协议的各字段长度固定,用readn来读是非常方便的。例如设计一种客户端上传文件的协议,规定前12字节表示文件名,超过12字节的文件名截断,不足12字节的文件名用'\0'补齐,从第13字节开始是文件内容,上传完所有文件内容后关闭连接,服务器可以先调用readn读12个字节,根据文件名创建文件,然后在一个循环中调用read读文件内容并存盘,循环结束的条件是read返回0。字段长度固定的协议往往不够灵活,难以适应新的变化。常见的应用层协议都是带有可变长字段的,字段之间的分隔符用换行的比用'\0'的更常见,例如http协议。下面自行编写的readLine函数也很有用,可以用来处理可变长字符:

static ssize_t my_read(int fd, char *ptr){    static int read_cnt;    static * read_ptr;    static read_buf[100];    if (read_cnt <= 0) {        again:        if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0 ) {            if (errno == EINTR)            goto again;            return -1;        }         else if (read_cnt == 0)            return 0;        read_ptr = read_buf;    }    read_cnt--;    *ptr = *read_ptr++;    return 1;}ssize_t Readline(int fd, void *vptr, size_t maxlen){    ssize_t n, rc;    char c, *ptr;    ptr = vptr;    for (n = 1; n < maxlen; n++) {        if ( (rc = my_read(fd, &c)) == 1 ) {        *ptr++ = c;            if (c == '\n')                break;        } else if (rc == 0) {            *ptr = 0;            return n - 1;        }else            return -1;    }    *ptr = 0;    return n;}


并发的接受请求, 可以使用并发处理多个client的请求,网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。

大体的代码框架如下所示:

listenfd = socket(...);bind(listenfd, ...);listen(listenfd, ...);while (1) {<span style="white-space:pre"></span>connfd = accept(listenfd, ...);<span style="white-space:pre"></span>n = fork();<span style="white-space:pre"></span>if (n == -1) {<span style="white-space:pre"></span>perror("call to fork");<span style="white-space:pre"></span>exit(1);<span style="white-space:pre"></span>} else if (n == 0) {<span style="white-space:pre"></span>close(listenfd);<span style="white-space:pre"></span>while (1) {<span style="white-space:pre"></span>read(connfd, ...);<span style="white-space:pre"></span>...<span style="white-space:pre"></span>write(connfd, ...);<span style="white-space:pre"></span>}<span style="white-space:pre"></span>close(connfd);<span style="white-space:pre"></span>exit(0);<span style="white-space:pre"></span>} else<span style="white-space:pre"></span>close(connfd);}



select的使用:

select是网络程序中很常用的一个系统调用,它可以同时监听多个阻塞的文件描述符(例如多个网络连接),哪个有数据到达就处理哪个,这样,不需要fork和多进程就可以实现并发服务的server。

下面是一个使用select的例子:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <netinet/in.h>#include "wrap.h"#define MAXLINE 80#define SERV_PORT 8000int main(int argc, char **argv){    int i, maxi, maxfd, listenfd, connfd, sockfd;    int nready, client[FD_SETSIZE];    ssize_t n;    fd_set rset, allset;    char buf[MAXLINE];    char str[INET_ADDRSTRLEN];    socklen_t cliaddr_len;    struct sockaddr_in    cliaddr, servaddr;    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);    maxfd = listenfd;    /* initialize */    maxi = -1;    /* index into client[]     * array */    for (i = 0; i < FD_SETSIZE; i++)    client[i] = -1; /* -1 indicates available entry */    FD_ZERO(&allset);    FD_SET(listenfd, &allset);    for ( ; ;  ) {        rset = allset; /* structure assignment */        nready = select(maxfd+1, &rset, NULL, NULL, NULL);        if (nready < 0)        perr_exit("select error");        if (FD_ISSET(listenfd, &rset)) { /* new client connection */            cliaddr_len = sizeof(cliaddr);            connfd = Accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);            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 < FD_SETSIZE; i++)            if (client[i] < 0) {                client[i] = connfd; /* savedescriptor */                break;            }            if (i == FD_SETSIZE) {                fputs("too many clients\n", stderr);                exit(1);            }            FD_SET(connfd, &allset);            /* add new descriptor to set */            if (connfd > maxfd)                maxfd = connfd; /* for select */            if (i > maxi)                maxi = i;            /* max index in* client[] array */            if (--nready == 0)                continue;            /* no more readable* descriptors */            for (i = 0; i <= maxi; i++) {            /* check all* clients for data */            if ( (sockfd = client[i]) < 0 )                continue;            if (FD_ISSET(sockfd, &rset)) {                if ( (n = Read(sockfd, buf,MAXLINE)) == 0 ) {                    /* connection closed by* client */                    Close(sockfd);                    FD_CLR(sockfd, &allset);                    client[i] = -1;                } else {                    int j;                    for (j = 0; j < n; j++)                    buf[j] =                    toupper(buf[j]);                    Write(sockfd, buf, n);                }                if (--nready == 0)                break; /* no more readable                descriptors */            }        }    }}


UDP通讯的流程如下所示:


//server.cint main(void){    struct sockaddr_in servaddr, cliaddr;    socklen_t cliaddr_len;    int sockfd;    char buf[MAXLINE];    char str[INET_ADDRSTRLEN];    int i, n;    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);//DGRAM代表的是数据报    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port = htons(SERV_PORT);    Bind(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr));    printf("Accepting connections ...\n");    while (1) {        cliaddr_len = sizeof(cliaddr);        n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);        if (n == -1)            perr_exit("recvfrom error");        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]);        n = sendto(sockfd, buf, n, 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));        if (n == -1)            perr_exit("sendto error");    }}//client.c#include <stdio.h>#include <string.h>#include <unistd.h>#include <netinet/in.h>#include "wrap.h"#define MAXLINE 80#define SERV_PORT 8000int main(int argc, char *argv[]){    struct sockaddr_in servaddr;    int sockfd, n;    char buf[MAXLINE];    char str[INET_ADDRSTRLEN];    socklen_t servaddr_len;    sockfd = Socket(AF_INET, SOCK_DGRAM, 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);    while (fgets(buf, MAXLINE, stdin) != NULL) {        n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));        if (n == -1)            perr_exit("sendto error");        n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);        if (n == -1)            perr_exit("recvfrom error");        Write(STDOUT_FILENO, buf, n);    }    Close(sockfd);    return 0;}



0 0