Linux socket 网络编程入门

Linux下的网络编程一般即是指socket套接字编程,入门比较矮简单,网上也有很多入门的例程。不过每次看过用过以后过段时间又忘了具体的操作了,又得去查,所以在这里总结整理一下,也省了以后查别人教程的时间。

1. socket套接字流程简介

socket套接字包含标准套接字(SOCK_STREAM,SOCK_DRAGM)以及原始套接字(SOCK_RAW),一般我们进行网络编程有标准套接字就够了,但如果要实现标准套接字(即TCP,UDP套接字)不能实现的功能,就需要用原始套接字了。这里还是主要总结一下标准套接字的用法。
如前所述,标准套接字分为TCP协议(SOCK_STREAM)和UDP协议(SOCK_DRAGM)两种type的工作流程,因为TCP是面向连接的服务,所以TCP网络编程会更复杂一点。不过不论是TCP还是UDP,其socket网络编程模式都是类似的,分为客户端和服务端。

  • 客户端:在网络程序中,如果一个程序主动和外面的程序通信,那么我们把这个程序称为客户端程序。
  • 服务端: 和客户端相对应的程序即为服务端程序。被动的等待外面的程序来和自己通讯的程序称为服务端程序。

一般在网络应用中,获取服务的客户即是客户端,提供服务的服务器即是服务端,不过也有些程序是互为服务和客户端,这种情况下, 一个程序既是客户端也是服务端。 以下两图描述了TCP和UDP socket通信的流程:

  • TCP socket流程
    Linux socket 网络编程入门_第1张图片

  • UDP socket流程
    Linux socket 网络编程入门_第2张图片

可以看到,因为TCP是面向连接的服务,包含三次握手建立连接的过程,所以TCP的服务器模式比UDP多了listen,accept函数,TCP客户端模式比UDP多了connect函数。

2. socket标准套接字基本函数介绍

Linux socket 网络编程入门_第3张图片

这张图将TCP数据交互的流程与socket函数作了一一对应,十分清楚,接下来就对其中的函数做一个整理介绍。

2.1 创建socket套接字

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

功能介绍:

在Linux操作系统中,一切皆文件,网络程序通过socket和其它几个函数的调用,会返回一个 通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的好处。。socket函数完成正确的操作是返回值大于0的文件描述符,当返回小于0的值时,操作错误。同样是返回一个文件描述符,但是会因为三个参数组合不同,对于数据具体的工作流程不同,对于应用层编程来说,这些也是不可见的。

参数说明:

  • family:说明我们网络程序所在的主机采用的通讯协族(AF_INET和AF_UNIX等)。
    AF_UNIX只能够用于单一的Unix 系统进程间通信,
    而AF_INET是针对Internet的,因而可以允许在远程
  • type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM,SOCK_RAW等)
    SOCK_STREAM表明我们用的是TCP 协议,这样会提供按顺序的,可靠,双向,面向连接的比特流。
    SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信。
  • protocol:具体的协议,对于标准套接字来说,其值是0,对于原始套接字来说就是具体的协议值。

2.2 地址端口绑定函数bind

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

功能介绍:

bind函数主要应用于服务器模式一端,其主要的功能是将addrlen长度 struct sockaddr类型的myaddr地址与sockfd文件描述符绑定到一起,在sockaddr中主要包含服务器端的协议族类型,网络地址和端口号等。在客户端模式中不需要使用bind函数。当bind函数返回0时,为正确绑定,返回-1,则为绑定失败。

参数说明:

  • sockfd:是由socket调用返回的文件描述符。
  • my_addr:是一个指向sockaddr的指针,由于struct sockaddr数据结构类型不方便设置,所以通常会通过对truct sockaddr_in进行数值结构设置,然后进行强制类型转换成struct sockaddr类型的数据,下面有两种类型数据结构的定义和对应关系图。
  • addrlen:是sockaddr结构的长度。
typedef unsigned short  sa_family_t;
struct in_addr {
    __be32    s_addr;
};


struct sockaddr {
    sa_family_t    sa_family;       /* address family, AF_xxx       */
    char           sa_data[14];     /* 14 bytes of protocol address */
};

/* Structure describing an Internet (IP) socket address.            */
#define __SOCK_SIZE__    16         /* sizeof(struct sockaddr)      */
struct sockaddr_in {
  sa_family_t       sin_family;     /* Address family               */
  __be16            sin_port;       /* Port number                  */
  struct in_addr    sin_addr;       /* Internet address             */

  /* Pad to size of `struct sockaddr'。                  */
  unsigned char     __pad[__SOCK_SIZE__ - sizeof(short int) -
            sizeof(unsigned short int) - sizeof(struct in_addr)];
};
  • struct sockaddr_in和struct sockaddr的映射关系
    Linux socket 网络编程入门_第4张图片

2.3 监听本地端口listen

int listen(int sockfd, int backlog)

功能介绍:

刚开始理解listen函数会有一个误区,就是认为其操作是在等在一个新的connect的到来,其实不是这样的,真正等待connect的是accept操作,listen的操作就是当有较多的client发起connect时,server端不能及时的处理已经建立的连接,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度有listen中的backlog参数来设定。listen和accept函数是服务器模式特有的函数,客户端不需要这个函数。当listen运行成功时,返回0;运行失败时,返回值位-1。

参数说明:

  • sockfd:是由socket调用返回的文件描述符
  • backlog:server端可以缓存连接的最大个数,也就是等待队列的长度。

2.4 接受网络请求函数accept

int accept(int sockfd, struct sockaddr *client_addr, socklen_t *len)

功能介绍:

接受函数accept其实并不是真正的接受,而是客户端向服务器端监听端口发起的连接。对于TCP来说,accept从阻塞状态返回的时候,已经完成了三次握手的操作。Accept其实是取了一个已经处于connected状态的连接,然后把对方的协议族,网络地址以及端口都存在了client_addr中,返回一个用于操作的新的文件描述符,该文件描述符表示客户端与服务器端的连接,通过对该文件描述符操作,可以向client端发送和接收数据。同时之前socket创建的sockfd,则继续监听有没有新的连接到达本地端口。返回大于0的文件描述符则表示accept成功,否则失败。

参数说明:

  • sockfd:是由socket调用返回的文件描述符
  • client_addr是本地服务器端的一个struct sockaddr类型的变量,用于存放新连接的客户端的协议族,网络地址以及端口号等,是用来给客户端的程序填写的,无需服务端填写
  • len:是第二个参数所指内容的长度,对于TCP来说其值可以用sizeof(struct sockaddr_in)来计算大小,说要说明的是accept的第三个参数要是指针的形式,因为这个值是要传给协议栈使用的。

2.5 连接目标服务器函数connect

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)

功能介绍:

连接函数connect是属于client端的操作函数,其目的是向服务器端发送连接请求,这也是从客户端发起TCP三次握手请求的开始,服务器端的协议族,网络地址以及端口都会填充到connect函数的serv_addr地址当中。当connect返回0时说明已经connect成功,返回值是-1时,表示connect失败。

参数说明:

  • sockfd:是由socket调用返回的文件描述符
  • serv_addr:是一个struct sockaddr类型的指针,这个参数中设置的是要连接的目标服务器的协议族,网络地址以及端口号
  • addrlen:表示第二个参数内容的大小,与accept不同,这个值不是一个指针

以上是TCP建立连接所需的五个函数(UDP所需函数也一样,只不过因为没有连接所以不需要listen,accept,connect函数)。在服务器端和客户端建立连接之后是进行数据间的发送和接收,TCP主要使用的接收函数是read或recv,发送函数是write或send;而UDP使用的则是recvfrom和sendto。

2.6 TCP写函数write

ssize_t write(int sockfd, const void *buf, size_t nbytes)

功能介绍:

write函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1。 并设置errno变量。

参数说明:

  • sockfd:对于服务端是accept调用返回的文件描述符,对于客户端是由socket调用返回的文件描述符
  • buf:发送数据缓冲区,要发送的数据会放在这个指针指向的内容空间中;
  • nbytes:发送缓冲区的大小

2.7 TCP读函数read

ssize_t read(int sockfd, const void *buf, size_t nbytes)

功能介绍:

read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误。

参数说明:

  • sockfd:对于服务端是accept调用返回的文件描述符,对于客户端是由socket调用返回的文件描述符
  • buf:用于存储接收到的数据缓冲区,接收的数据将放到这个指针所指向的内容的空间中
  • nbytes:接收缓冲区的大小

2.8 TCP发送及读取数据函数send,recv

int recv(int sockfd, void *buf, int len, int flags)
int send(int sockfd, void *buf, int len, int flags)

功能介绍:

recv和send函数提供了和read和write差不多的功能。不过它们提供 了第四个参数来控制读写操作。

参数说明:

前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合 (如果flags为0,则和read,write一样的操作。还有其它的几个选项,不过我们实际上用的很少)

MSG_DONTROUTE 不查找路由表
MSG_OOB 接受或者发送带外数据
MSG_PEEK 查看数据,并不从系统缓冲区移走数据
MSG_WAITALL 等待所有数据

2.9 UDP发送数据函数sendto

ssize_t sendto(int sockfd, const void *buf, size_t len, int flag, const struct sockaddr *to, socklen_t tolen)

功能介绍:

sendto函数主要根据填充的接收方的地址信息向客户端或者服务器端发送数据,接收方的地址信息会提前设置在struct sockaddr类型的参数指针中,当返回值-1时,表明发送失败,当返回值大于等于0时,表示发送成功,并且发送数据的大小会通过返回值传递回来。

参数说明:

  • sockfd:由socket创建的文件描述符;
  • buf:发送数据缓冲区,要发送的数据会放在这个指针指向的内容空间中;
  • len:发送缓冲区的大小;
  • to:一个struct sockaddr类型的指针,其指向地址的内容是接收方地址信息;
  • tolen:表示第5个参数指向的数据内容的长度,传递的是值,可以用sizeof(struct sockaddr)计算。

2.10 UDP接收数据函数recvfrom

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)

功能介绍:

对于该函数主要的功能是,从客户端或者服务器端接收数据以及发送方的地址信息存储到本地的struct sockaddr类型参数变量当中,如果函数返回-1,所说明接收数据失败,如果返回的是大于等于0的值,则说明函数接收到的数据的大小。因为可以设置文件描述符的状态为阻塞模式,所以在没有接收到数据时,recvfrom会一直处于阻塞状态,直到有数据接收到。

参数说明:

  • sockfd:创建socket时的文件描述符
  • buf:用于存储接收到的数据缓冲区,接收的数据将放到这个指针所指向的内容的空间中
  • len:接收缓冲区的大小
  • from:指向struct sockaddr的指针,接收发送方地址信息
  • fromlen:表示第5个参数所指向内容的长度,可以使用sizeof(struct sockaddr)来定义大小,不过因为是要传给内核协议栈,所以使用了指针类型

数据的发送和接收结束以后需要关闭套接字,关闭套接字有两个函数close和shutdown。TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希望只关闭一个方向,这个时候我们可以使用shutdown。针对不同的howto,系统回采取不同的关闭方式。

2.11 关闭函数close

int close(int sockfd)

功能介绍:

当我们使用close时,会把读写通道都关闭。不过,close只会关闭本进程的 socket id,但连接还是开着的,用这个socket id的其他进程还能用这个连接进行读写。使用close中止一个连接,其实它只是减少文件描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭。

参数说明:

  • sockfd:创建socket时的文件描述符

2.12 关闭函数shutdown

int shutdown(int sockfd, int howto)

功能介绍:

shutdown函数针对不同的howto,系统回采取不同的关闭方式,可以选择只中止一个方向的连接。与close不同的是,shutdown不考虑文件描述符的参考数,直接关闭文件描述符。在多进程程序里面,如果有几个子进程共享一个套接字时,如果我们使用shutdown,那么所有的子进程都不能够操作了,这个时候若想只关闭其中一个子进程的套接字描述符我们只能够使用close。

参数说明:

  • sockfd:创建socket时的文件描述符
  • howto:针对不同的howto,系统回采取不同的关闭方式
    howto=0这个时候系统会关闭读通道,但是可以继续往接字描述符写
    howto=1关闭写通道,和上面相反,着时候就只可以读了
    howto=2关闭读写通道,和close一样

3. 简单实例

  • server端:
server.c

====================================================================

#include     // for sockaddr_in
#include     // for socket
#include     // for socket
#include         // for printf
#include         // for exit
#include         // for bzero
/*
#include 
#include 
#include 
#include 
*/
#define HELLO_WORLD_SERVER_PORT    6666 
#define LENGTH_OF_LISTEN_QUEUE 20
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512

int main(int argc, char **argv)
{
    //设置一个socket地址结构server_addr,代表服务器internet地址, 端口
    struct sockaddr_in server_addr;
    bzero(&server_addr,sizeof(server_addr)); //把一段内存区的内容全部设置为0
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htons(INADDR_ANY);
    server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);

    //创建用于internet的流协议(TCP)socket,用server_socket代表服务器socket
    int server_socket = socket(PF_INET,SOCK_STREAM,0);
    if( server_socket < 0)
    {
        printf("Create Socket Failed!");
        exit(1);
    }
{ 
   int opt =1;
   setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
}

    //把socket和socket地址结构联系起来
    if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr)))
    {
        printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT); 
        exit(1);
    }

    //server_socket用于监听
    if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) )
    {
        printf("Server Listen Failed!"); 
        exit(1);
    }
    while (1) //服务器端要一直运行
    {
        //定义客户端的socket地址结构client_addr
        struct sockaddr_in client_addr;
        socklen_t length = sizeof(client_addr);

        //接受一个到server_socket代表的socket的一个连接
        //如果没有连接请求,就等待到有连接请求--这是accept函数的特性
        //accept函数返回一个新的socket,这个socket(new_server_socket)用于同连接到的客户的通信
        //new_server_socket代表了服务器和客户端之间的一个通信通道
        //accept函数把连接到的客户端信息填写到客户端的socket地址结构client_addr中
        int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length);
        if ( new_server_socket < 0)
        {
            printf("Server Accept Failed!\n");
            break;
        }

        char buffer[BUFFER_SIZE];
        bzero(buffer, BUFFER_SIZE);
        length = recv(new_server_socket,buffer,BUFFER_SIZE,0);
        if (length < 0)
        {
            printf("Server Recieve Data Failed!\n");
            break;
        }
        char file_name[FILE_NAME_MAX_SIZE+1];
        bzero(file_name, FILE_NAME_MAX_SIZE+1);
        strncpy(file_name, buffer, strlen(buffer)>FILE_NAME_MAX_SIZE?FILE_NAME_MAX_SIZE:strlen(buffer));
//        int fp = open(file_name, O_RDONLY);
//        if( fp < 0 )
        printf("%s\n",file_name);
        FILE * fp = fopen(file_name,"r");
        if(NULL == fp )
        {
            printf("File:\t%s Not Found\n", file_name);
        }
        else
        {
            bzero(buffer, BUFFER_SIZE);
            int file_block_length = 0;
//            while( (file_block_length = read(fp,buffer,BUFFER_SIZE))>0)
            while( (file_block_length = fread(buffer,sizeof(char),BUFFER_SIZE,fp))>0)
            {
                printf("file_block_length = %d\n",file_block_length);
                //发送buffer中的字符串到new_server_socket,实际是给客户端
                if(send(new_server_socket,buffer,file_block_length,0)<0)
                {
                    printf("Send File:\t%s Failed\n", file_name);
                    break;
                }
                bzero(buffer, BUFFER_SIZE);
            }
//            close(fp);
            fclose(fp);
            printf("File:\t%s Transfer Finished\n",file_name);
        }
        //关闭与客户端的连接
        close(new_server_socket);
    }
    //关闭监听用的socket
    close(server_socket);
    return 0;
}
  • client端:
client.c
====================================================================

#include     // for sockaddr_in
#include     // for socket
#include     // for socket
#include         // for printf
#include         // for exit
#include         // for bzero
/*
#include 
#include 
#include 
#include 
*/

#define HELLO_WORLD_SERVER_PORT    6666 
#define BUFFER_SIZE 1024
#define FILE_NAME_MAX_SIZE 512

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        printf("Usage: ./%s ServerIPAddress\n",argv[0]);
        exit(1);
    }

    //设置一个socket地址结构client_addr,代表客户机internet地址, 端口
    struct sockaddr_in client_addr;
    bzero(&client_addr,sizeof(client_addr)); //把一段内存区的内容全部设置为0
    client_addr.sin_family = AF_INET;    //internet协议族
    client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自动获取本机地址
    client_addr.sin_port = htons(0);    //0表示让系统自动分配一个空闲端口
    //创建用于internet的流协议(TCP)socket,用client_socket代表客户机socket
    int client_socket = socket(AF_INET,SOCK_STREAM,0);
    if( client_socket < 0)
    {
        printf("Create Socket Failed!\n");
        exit(1);
    }
    //把客户机的socket和客户机的socket地址结构联系起来
    if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr)))
    {
        printf("Client Bind Port Failed!\n"); 
        exit(1);
    }

    //设置一个socket地址结构server_addr,代表服务器的internet地址, 端口
    struct sockaddr_in server_addr;
    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    if(inet_aton(argv[1],&server_addr.sin_addr) == 0) //服务器的IP地址来自程序的参数
    {
        printf("Server IP Address Error!\n");
        exit(1);
    }
    server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
    socklen_t server_addr_length = sizeof(server_addr);
    //向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接
    if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0)
    {
        printf("Can Not Connect To %s!\n",argv[1]);
        exit(1);
    }

    char file_name[FILE_NAME_MAX_SIZE+1];
    bzero(file_name, FILE_NAME_MAX_SIZE+1);
    printf("Please Input File Name On Server:\t");
    scanf("%s", file_name);

    char buffer[BUFFER_SIZE];
    bzero(buffer,BUFFER_SIZE);
    strncpy(buffer, file_name, strlen(file_name)>BUFFER_SIZE?BUFFER_SIZE:strlen(file_name));
    //向服务器发送buffer中的数据
    send(client_socket,buffer,BUFFER_SIZE,0);

//    int fp = open(file_name, O_WRONLY|O_CREAT);
//    if( fp < 0 )
    FILE * fp = fopen(file_name,"w");
    if(NULL == fp )
    {
        printf("File:\t%s Can Not Open To Write\n", file_name);
        exit(1);
    }

    //从服务器接收数据到buffer中
    bzero(buffer,BUFFER_SIZE);
    int length = 0;
    while( length = recv(client_socket,buffer,BUFFER_SIZE,0))
    {
        if(length < 0)
        {
            printf("Recieve Data From Server %s Failed!\n", argv[1]);
            break;
        }
//        int write_length = write(fp, buffer,length);
        int write_length = fwrite(buffer,sizeof(char),length,fp);
        if (write_lengthprintf("File:\t%s Write Failed\n", file_name);
            break;
        }
        bzero(buffer,BUFFER_SIZE);    
    }
    printf("Recieve File:\t %s From Server[%s] Finished\n",file_name, argv[1]);

    close(fp);
    //关闭socket
    close(client_socket);
    return 0;
}

你可能感兴趣的:(Linux,tcp/ip,socket,c/c++)