Linux下Socket编程

网络编程,一定离不开套接口;那什么是套接口呢?在Linux下,所有的I/O操作都是通过读写文件描述符而产生的,文件描述符是一个和打开的文件相关联的整数,这个文件并不只包括真正存储在磁盘上的文件,还包括一个网络连接、一个命名管道、一个终端等,而套接口就是系统进程和文件描述符通信的一种方法。目前最常用的套接口是字:字节流套接口(基于TCP)和数据报套接口(基于UDP),当然还有原始套接口(原始套接口提供TCP套接口和UDP套接口所不提供的功能,如构造自己的TCP或UDP分组)等,我们这里主要介绍字节流套接口和数据报套接口。
要学习网络编程,一定离不开网络库的函数,在Linux系统下,可以用”man 函数名”来得到这个函数的帮助,不过为了照顾E文不大好的朋友,下面就将常用的网络函数和用法列出来供大家参考:
1、socket函数:为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。
————————————————————————————-
#include 
#include 
int socket(int family,int type,int protocol);
返回:非负描述字---成功   -1---失败
————————————————————————————-
第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);第二个参数指明套接口类型,有三种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)和SOCK_RAW(原始套接口);如果套接口类型不是原始套接口,那么第三个参数就为0。

2、connect函数:当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据。
————————————————————————————-
#include 
#include 
int connect(int sockfd,const struct sockaddr * serv_addr,int addrlen);
返回:0---成功   -1---失败
————————————————————————————-
第一个参数是socket函数返回的套接口描述字;第二和第三个参数分别是一个指向套接口地址结构的指针和该结构的大小。
这些地址结构的名字均已“sockaddr_”开头,并以对应每个协议族的唯一后缀结束。以IPv4套接口地址结构为例,它以“sockaddr_in”命名,定义在头文件;以下是结构体的内容:
————————————————————————————-
struct in_addr {
unsigned long s_addr;      /* IPv4地址 */
};
struct sockaddr_in {
short int sin_family; /* 套接口地址结构的地址簇,这里为AF_INET */
unsigned short int sin_port; /* TCP或UDP端口 */
struct in_addr sin_addr;/*存放ip地址的结构*/
unsigned char sin_zero; /* 无符号的8位整数 */
};
————————————————————————————-

3、bind函数:为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
————————————————————————————-
#include 
#include 
int bind(int sockfd,const struct sockaddr *myaddr,int addrlen);
返回:0---成功   -1---失败
————————————————————————————-
第一个参数是socket函数返回的套接口描述字;第二和第第三个参数分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。

4、listen函数:listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。
————————————————————————————-
#include 
#include 
int listen(int sockfd,int backlog);
返回:0---成功   -1---失败
————————————————————————————-
第一个参数是socket函数返回的套接口描述字;第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:以完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。

5、accept函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
————————————————————————————-
#include 
#include 
int accept(int sockfd,struct sockaddr *iaddr,int *addrlen);
返回:非负描述字---成功   -1---失败
————————————————————————————-
第一个参数是socket函数返回的套接口描述字;第二个和第三个参数分别是一个指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字;如果对客户段的信息不感兴趣,可以将第二和第三个参数置为空。

6、inet_pton函数:将点分十进制串转换成网络字节序二进制值,此函数对IPv4地址和IPv6地址都能处理。
————————————————————————————-
#include
int inet_pton(int family,const char * strptr,void * addrptr);
返回:1---成功   0---输入不是有效的表达格式   -1---失败
————————————————————————————-
第一个参数可以是AF_INET或AF_INET6:第二个参数是一个指向点分十进制串的指针:第三个参数是一个指向转换后的网络字节序的二进制值的指针。

7、inet_ntop函数:和inet_pton函数正好相反,inet_ntop函数是将网络字节序二进制值转换成点分十进制串。
————————————————————————————-
#include
const char * inet_ntop(int family,const void * addrptr,char * strptr,size_t len);
返回:指向结果的指针---成功   NULL---失败
————————————————————————————-
第一个参数可以是AF_INET或AF_INET6:第二个参数是一个指向网络字节序的二进制值的指针;第三个参数是一个指向转换后的点分十进制串的指针;第四个参数是目标的大小,以免函数溢出其调用者的缓冲区。

8、fock函数:在网络服务器中,一个服务端口可以允许一定数量的客户端同时连接,这时单进程是不可能实现的,而fock就分配一个子进程和客户端会话,当然,这只是fock的一个典型应用。
————————————————————————————-
#include
pid_t fock(void);                                 返回:在子进程中为0,在父进程中为子进程ID   -1---失败
————————————————————————————-
fock函数调用后返回两次,父进程返回子进程ID,子进程返回0。

有了上面的基础知识,我们就可以进一步了解TCP套接口和UDP套接口

1、TCP套接口
TCP套接口使用TCP建立连接,建立一个TCP连接需要三次握手,基本过程是服务器先建立一个套接口并等待客户端的连接请求;当客户端调用connect进行主动连接请求时,客户端TCP发送一个SYN,告诉服务器客户端将在连接中发送的数据的初始序列号;当服务器收到这个SYN后也给客户端发一个SYN,里面包含了服务器将在同一连接中发送的数据的初始序列号;最后客户在确认服务器发的SYN。到此为止,一个TCP连接被建立。
下面就用一个例子来说明服务器和客户是怎么连接的
————————————————————————————-

/* client.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h> /*struct sockaddr_in 结构定义在这里*/
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 3049 /*服务器端口监听号*/
 
void doclient(FILE *fp,int sockfd);
main(int argc,char*argv[])
{
    int sockfd,n;
    struct hostent *he;/*???????*/
    struct sockaddr_in server_addr;/*connector's address*/
    if (argc!=2)
    {
        fprintf(stderr,"usage:client hostname \n");
        exit(1);
    }
 
    printf("gethostbyname begin\n");
    if ((he =(struct hostent*)gethostbyname(argv[1]))==NULL)
    {
        herror("gethostbyname");
        exit(1);
    }
    printf("gethostbyname end\n");
    /*建立与服务器的连接*/
    if ((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
    {
        perror("socket");
        exit(1);
    }
    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    server_addr.sin_port=htons(PORT);
    //server_addr.sin_addr=((struct in_addr*)he->h_addr);
    inet_pton(AF_INET,argv[1],&server_addr.sin_addr);
    if (connect(sockfd,(struct sockaddr*)&server_addr,sizeof(struct sockaddr))==-1)
    {
        perror("connect");
        exit(1);
    }
    doclient(stdin,sockfd);
    return(0);
}
void doclient(FILE *fp,int sockfd)
{
    char sendline[2047],recvline[2048];
    int n;
    /*下面进入处理循环,当接收到文件结束符Ctrl+D时跳出*/
    do{
        if ((n=read(sockfd,recvline,2047))==0)
        {
            perror("read");
            exit(1);
        }
        recvline[n]=0;/*给结尾加上0*/
        fputs(recvline,stdout);
        if (fgets(sendline,2048,fp)!=NULL)
        {
            write(sockfd,sendline,strlen(sendline));
        }
        else
        {
            break;
                                            }
    }while(1);
 
}
/* server.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MYPORT 3049 /*服务器端口监听号*/
#define BACKLOG 10 /*最大同时连接请求数*/
void sig_chld();
void echoclient(int);
 
int main()
{
    int listenfd,new_fd;
    struct sockaddr_in my_addr;
    struct sockaddr_in their_addr;
    int sin_size;
 
    /*建立监听套接口*/
    if ((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
    {
        perror("socket");
        exit(1);
    }
    bzero(&(my_addr),sizeof(my_addr));
            /*void bzero(void *s, int n);置字节字符串s的前n个字节为零*/
        my_addr.sin_family=AF_INET;/*internet域的地址族*/
    my_addr.sin_port=htons(MYPORT);/*hostshort 转换成网络字节顺序 */
    my_addr.sin_addr.s_addr=INADDR_ANY;
            /*通配ip地址,告诉系统选择本地机的地址*/
 
        if (bind(listenfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))==-1)
        {
            perror("blind");
            exit(1);
        }
    if (listen(listenfd,BACKLOG)==-1)
    {
        perror("listen");
        exit(1);
    }
    signal(SIGCHLD,sig_chld);/*处理子进程终止信号,避免僵尸进程*/
    /*void (*signal(int,void(*)(int))(int);*/
 
    for(;;)/* accept loop,循环处理listen中的客户请求队列*/
    {
        sin_size = sizeof(struct sockaddr_in);
        if ((new_fd=accept(listenfd,(struct sockaddr*)&their_addr,&sin_size))==-1)
        {
            if (errno==EINTR)/*EINTR:当睡眠时接收到其他信号*/
            {
                continue;/*accept 调用可能被信号打断*/
            }
            perror("accept");
            continue;
        }
        printf("server:got connection from%s\n",inet_ntoa(their_addr.sin_addr));
        /*char * inet_ntoa( struct in_addr in); 将网络地址转换成“.”点隔的字符串格式*/
 
        if (!fork())/* fork出子进程处理客户请求*/
        {
            close(listenfd);
            if (send(new_fd,"Hello world!",13,0)==-1)
            {
                perror("send");
            }
            echoclient(new_fd);/*调用函数,信息返回给客户*/
            exit(0);/*0时正常退出,1时因错误退出*/
        }
        close(new_fd);
                        /*父进程关闭刚才的套接口,并继续处理其他的客户请求*/
    }
}
void sig_chld()/*SIGHLD 信号捕捉函数*/
{
    pid_t pid;
            /*:typedef short pid_t;表示的是内核中的进程表的索引 */
        int stat;
 
    while((pid=waitpid(-1,&stat,WNOHANG))>0)
                /*处理已经结束的子进程,避免僵尸进程*/
            /*
               pid_t waitpid(pid_t pid, int *statloc, int options);
               正常情况下返回pid,
                     或者0(waitpid在非block模式下才有可能返回),-1代表错误
               WNOHANG:如果还没有退出,不block,返回0;
               statloc参数保存了退出进程的状态*/
            printf("chile %d terminated\n",pid);/*打印“进程终止”*/
    return;
}
void echoclient(int sockfd)
{
    int n;
    char buf[2048];
    for(;;)
    {
        if ((n=read(sockfd,buf,sizeof(buf)))<=0)
        {
            break;/*对方关闭连接时,跳出循环*/
        }
        write(sockfd,buf,n);
                        /*把接收自客户的数据,原本不动的发送给客户*/
    }
    close(sockfd);
}

————————————————————————————-
现在让我们来编译这两个程序:
root@linuxaid#gcc -o server server.c
root@linuxaid#gcc -o client client.c
然后在一台计算机上先运行服务器程序,再在另一个终端上运行客户端就会看到结果。
————————————————————————————-
建立一个TCP连接需要三次握手,而断开一个TCP则需要四个分节。当某个应用进程调用close(主动端)后(可以是服务器端,也可以是客户端),这一端的TCP发送一个FIN,表示数据发送完毕;另一端(被动端)发送一个确认,当被动端待处理的应用进程都处理完毕后,发送一个FIN到主动端,并关闭套接口,主动端接收到这个FIN后再发送一个确认,到此为止这个TCP连接被断开。

2、UDP套接口

————————————————————————————-

/* server.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
 
#define MAXLINE 80
#define SERV_PORT 8888
 
void do_echo(int sockfd, struct sockaddr *pcliaddr, socklen_t clilen)
{
    int n;
    socklen_t len;
    char mesg[MAXLINE];
 
    for(;;)
    {
        len = clilen;
        /* waiting for receive data */
        n = recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
        /* sent data back to client */
        sendto(sockfd, mesg, n, 0, pcliaddr, len);
    }
}
 
int main(void)
{
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
 
    sockfd = socket(AF_INET, SOCK_DGRAM, 0); /* create a socket */
 
    /* init servaddr */
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
 
    /* bind address and port to socket */
    if(bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        perror("bind error");
        exit(1);
    }
 
    do_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
 
    return 0;
}
/* client.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
 
#define MAXLINE 80
#define SERV_PORT 8888
 
void do_cli(FILE *fp, int sockfd, struct sockaddr *pservaddr, socklen_t servlen)
{
    int n;
    char sendline[MAXLINE], recvline[MAXLINE + 1];
 
    /* connect to server */
    if(connect(sockfd, (struct sockaddr *)pservaddr, servlen) == -1)
    {
        perror("connect error");
        exit(1);
    }
 
    while(fgets(sendline, MAXLINE, fp) != NULL)
    {
        /* read a line and send to server */
        write(sockfd, sendline, strlen(sendline));
        /* receive data from server */
        n = read(sockfd, recvline, MAXLINE);
        if(n == -1)
        {
            perror("read error");
            exit(1);
        }
        recvline[n] = 0; /* terminate string */
        fputs(recvline, stdout);
    }
}
 
int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in srvaddr;
 
    /* check args */
    if(argc != 2)
    {
        printf("usage: udpclient <IPaddress>\n");
        exit(1);
    }
 
    /* init servaddr */
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        printf("[%s] is not a valid IPaddress\n", argv[1]);
        exit(1);
    }
 
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
 
    do_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
 
    return 0;
}

————————————————————————————-

1、编译例子程序
使用如下命令来编译例子程序:
gcc -Wall -o udpserv udpserv.c
gcc -Wall -o udpclient udpclient.c
编译完成生成了udpserv和udpclient两个可执行程序。

2、运行UDP Server程序
执行./udpserv &命令来启动服务程序。我们可以使用netstat -ln命令来观察服务程序绑定的IP地址和端口,部分输出信息如下:
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:32768 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:6000 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN
udp 0 0 0.0.0.0:32768 0.0.0.0:*
udp 0 0 0.0.0.0:8888 0.0.0.0:*
udp 0 0 0.0.0.0:111 0.0.0.0:*
udp 0 0 0.0.0.0:882 0.0.0.0:*
可以看到udp处有“0.0.0.0:8888”的内容,说明服务程序已经正常运行,可以接收主机上任何IP地址且端口为8888的数据。
如果这时再执行./udpserv &命令,就会看到如下信息:
bind error: Address already in use
说明已经有一个服务程序在运行了。

3、运行UDP Client程序
执行./udpclient 127.0.0.1命令来启动客户程序,使用127.0.0.1来连接服务程序,执行效果如下:
Hello, World!
Hello, World!
this is a test
this is a test
^d
输入的数据都正确从服务程序返回了,按ctrl+d可以结束输入,退出程序。
如果服务程序没有启动,而执行客户程序,就会看到如下信息:
$ ./udpclient 127.0.0.1
test
read error: Connection refused
说明指定的IP地址和端口没有服务程序绑定,客户程序就退出了。这就是使用connect()的好处,注意,这里错误信息是在向服务程序发送数据后收到的,而不是在调用connect()时。如果你使用tcpdump程序来抓包,会发现收到的是ICMP的错误信息。