带你从零学习linux下的socket编程

来源:互联网 发布:西门子vb 编辑:程序博客网 时间:2024/05/16 08:21

      我们在平时的开发过程中很少接触到socket开发,以前在学习java基础的时候这个socket编程的学习也是一带而过,没有怎么深入的研究学习,现在感觉还是很有必要的去深入了解一下socket。由于现在从事Android开发,Android底层又是基于linux系统的,所以我接下来就聊一下linux下的socket编程,好了,闲话不多说了,进入正题。

一切皆socket

      在早期的单机系统中,各进程都是运行在自己的地址空间里面,进程之间互不干扰。操作系统为了解决进程之间可以相互通信并且又不互相干扰,为进程之间提供了多种通信机制,比如说管道、命名管道、消息队列、共享内存和信号量等。但是这些机制都仅限于本机的进程通信,如果要解决网络间的进程通信怎么做呢?
      在单机上面,两个进程之间只要知道进程id就好办,但是在网络间知道进程的id是没有用的。举个例子,必须要让对方进程知道,我(进程)现在在哪个地址(ip)、哪个地方见面(端口)以及见面要遵循哪样的方式(协议)。也就是说网络间的通信离不开ip、协议、端口这三个要素,这三要素就可以标识网络的一个进程了。而这个协议现在用的最多的就是tcp/ip协议了,使用tcp/ip协议的应用程序通常采用的编程接口有socket和tci来实现进程的通信,就目前而言,tci已经被淘汰了,所以几乎所有的应用程序都是采用socket来编程了。

什么是socket?

      socket可以看成是用户进程与内核网络协议栈的编程接口。socket不仅可以用于单机进程之间的通信,也可以用在网络进程之间的通信。socket作为一个编程接口在网络中所处的位置如下图:

这里写图片描述

说白了socket其实就是应用程序和tcp/ip协议的中间抽象层,在编程设计模式里面这种类似于门面模式,就是socket只对应用程序提供一组接口,而把复杂的协议族隐藏在身后,协议对应用程序是透明的,socket负责组织好协议需要的数据格式。

socket api编程模型

这里写图片描述

socket之间的连接可以分为三个步骤:服务端监听、客户端请求、连接确认,从上面的模型中我么你可以看到,通信主要是依靠“write-read / read-write ”这种模型来进行数据的读写,既然是这种模型,那么socket拥有这样的api。下面以tcp协议为例讲述一下socket的使用方式。

socket中tcp建立通信的三次握手机制

客户端和服务端进行通信,要遵循tcp的三次握手机制,如下:

这里写图片描述

  • 客户端通过connect触发了连接请求,开始向服务器发送一个SYN J的数据包,接着connect函数进入阻塞状态等待返回。
  • 服务端监听到客户端连接的请求,并接收来自客户端发送过来的SYN J数据包,接着调用accept接收请求并向客户端发送 SYN K 和 ACK J+1的数据包,此时accept处于阻塞状态。
  • 客户端收到SYN K 和 ACK J+1的数据包时,connect函数返回,并对SYN K进行确认,确认完毕后,向服务器发送SYN k+1。

至此服务器收到SYN k+1的包后,accept函数返回。至此客户端和服务端的通信正式建立。

tcp服务端api

1. socket的创建

使用man socket命令查看socket函数的使用方式。函数原型如下

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

socket() creates an endpoint for communication and returns a descriptor.The domain argument specifies a communication domain; this selects theprotocol family which will be used for communication.
官方的描述是:socket()函数创建了一个通信的切入点,并且返回一个描述符,而domain参数制定了一个协议域,指定了哪种协议族被用来作为通信的协议,而type则制定了通信语义,也就是socket的类型,protocol这是指定使用哪种协议。下面分别介绍。
     a)domain:常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。由于现在基本上使用32位的ipv4地址,所以在 socket编程中domain一般设置为AF_INET,表明要用ipv4和端口号的组合协议族。
     b)type::指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
            流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用,可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
            数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用,不提供无错保证,数据可能接丢失,不能保证按顺序接收。
     c)protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
      这样创建了一个socket之后,仅仅只获取了这个socket的描述符。但是此时还并没有一个具体的地址,如果要指定一个地址,接下来就要调用bind函数,否则在调用下面的listen函数时候,系统会自动分配一个地址。

2. bind()

bind函数负责把一个协议族的特定地址赋给socket,例如AF_INET代表ipv4的地址和端口号的组合、AF_INET6代表ipv6的地址和端口号的组合。函数原型如下:

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

     a)sockfd参数:代表socket的套接字描述符,这个描述符就能代表一个socket,每个进程空间都有一个套接字描述符表,该表在那个存放着套接字描述符和套接字数据接口对应的关系,该表中有一个字段存放套接字的描述符,另外一个字段存放着套接字数据接口地址,因此根据套接字描述符就可以找到对应的套接字数据接口。
     b)addr参数:一个const struct sockaddr *指针,指定要绑定的协议族的地址是多少,协议族不同,这个地址的结构不同。
ipv4的协议族地址结构如下:

struct sockaddr_in {    sa_family_t    sin_family; // 协议族 比如所AF_INET    in_port_t      sin_port;   // 网络字节序中的端口    struct in_addr sin_addr;   // 网络地址};struct in_addr {    uint32_t  s_addr; // 网络字节序中的地址};

     c)addrlen参数:地址的长度

     d)网络字节序:是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,而主机字节序是和具体的主机平台有关联的,因此网络字节序可以保证数据在不同主机之间传输时能够被正解析,因此在使用的时候,协议族中的地址一定要从主机字节序转化成网络字节序。主机字节序就是我们平时说的大端和小端模式,不同的cpu有不同的字节序类型,这些字节序在内存中有不同的保存顺序。小端模式就意味着数据的低位字节放在内存的低地址处。如何查看自己的主机是大端模式还是小端模式呢?

#include<stdio.h>#include <arpa/inet.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>void  main(){    unsigned int data = 0x12345678;    char *p = &data;    if(p[0] == 0x78){        printf("小端模式\n");    }else if(p[0] == 0x12){        printf("大端模式\n");    }}

我电脑上的结果是:小端模式。
当我们知道主机的字节序类型之后,如何把主机字节序转化成网络字节序呢?这个时候可以参考我们的下列转化函数:
     htons把unsigned short类型从主机序转换到网络序
       htonl 把unsigned long类型从主机序转换到网络序
       ntohs 把unsigned short类型从网络序转换到主机序
       ntohl 把unsigned long类型从网络序转换到主机序
     e)在tcp/ip协议中,ipv4的网络地址是32位的,而我们编程中通常指定的ip地址是点分形式的,如:192.168.1.1。如何在点分ip和网络地址之间相互转换呢?

    // 若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址    in_addr_t inet_addr(const char *cp);    // 将一个网络IP转换成一个互联网标准点分格式的字符串    char *inet_ntoa(struct in_addr in);    // 将一个字符串IP地址转换为一个32位的网络序列IP地址    int inet_aton(const char *cp, struct in_addr *inp);

通过上述的三个函数则可以实现点分ip到网络地址之间的转换。

    char *myIp = "192.168.1.1";    int  addr = inet_addr(&myIp);    printf("%u\n",addr);    struct in_addr inp;    inet_aton(&myIp,&inp);    printf("%u\n",inp.s_addr);    char *inet_ntoa(inp);    printf("%s\n",inet_ntoa());
3. listen()

    在指定了协议族的地址之后,就需要监听这个地址的客户端请求。

int listen(int sockfd, int backlog)

     a)sockfd参数:代表的就是刚才所创建的socket的描述符,这里的的socket是一个被动的套接字,其只能用来监听,不能用来做其他任何的操作。
    b)The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. SOMAXCONN with the value 128。
backlog 参数定义了正在排队等待连接的socket描述符的最大个数,SOMAXCONN 表示 128。
上述几步操作之后,就可以接受客户端的请求了。

4. accept()

用于接受客户端的请求,建立两者之间的连接。

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

    a)sockfd是刚才创建的被动套接字的描述符。
    b)addr代表的是客户端的协议地址。
    c)协议地址的长度。
如果accept接受请求成功,那么会返回一个全新的socket,代表和客户端的一个tcp连接,并且是一个主动套接字,可以与客户端进行通信读写操作。如果此时没有客户端连接,这个函数将会被阻塞。read和write、close函数在下面介绍完connect函数后统一介绍

tcp客户端api

1. 建立socket

见tcp服务端socket创建过程。

2. connect函数

在服务器端调用 bind和accept函数后,服务器就可以接受客户端的请求了。此时客户端只需要调用connect函数与服务端进行连接,连接成功后,就可以与服务端进行正常的读写操作。

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

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
connect成功后,服务端的accept函数救护接受到请求,这个时候客户端就可以通过write写数据,服务器端就可以通过read读数据。

read/write函数api

当服务端经过listen,客户端经过connect后,此时两者就可以利用i/o进程通信了,网络io操作可以利用一下几组函数来实现:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()
    函数原型如下:
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);#include <sys/types.h>#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t recv(int sockfd, void *buf, size_t len, int flags);size_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。

  • write的返回值大于0,表示写了部分或者是全部的数据。
  • 返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

close()函数api

当客户端与服务端不再需要通信时候,可以调用close函数断开连接。

int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

socket中tcp的四次握手释放连接

经过上述的分析之后,读者应该知道两个进程之间是如何建立通信的、是如何发送接收数据的以及如何的关闭通信连接通道的。但是我们应该还需要明白的是socket连接是如何释放的?
上面我们也介绍过,socket的连接需要经过三次握手,那么释放则需要经过四次握手。

这里写图片描述

  • 当应用程序A方调用close函数主动关闭连接时,tcp会发送一个FIN M数据给应用程序B方。
  • B方接到FIN M之后,对 FIN M进行确认并执行被动关闭操作,接着向A发送ACK M+1数据,FIN数据的接收意味着B在相应的连接上面再也接收不到数据了,一段时间后,B会调用close主动关闭连接。
  • B关闭了与A之间的连接,发送FIN N给A。
  • A 接收到FIN N后,发送ACK N+1给B。
    至此两者之间的连接正式关闭。

那么为什么建立连接的时候只需要三次握手,而释放连接的时候却要经过四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

linux下的socket编程

Server.c

#include <sys/types.h> #include <sys/socket.h>#include <stdio.h>#include <netinet/in.h>#include <netinet/ip.h> void main(){    /*     domain : protocol  family  which will be used for communication    type   :  specifies the communication semantics 指明通信的语义     int socket(int domain, int type, int protocol);     */    int domain = AF_INET;    //  Provides sequenced, reliable, two-way, connection-based byte  streams.    int type = SOCK_STREAM;    int protocol = 0;    int socketFd = socket(domain,type,protocol);    if (socketFd != -1){        /* socket创建成功,bind        bind需要的是一个通用的地址结构,我们给定一个具体的ipv4的地址结构                    man 7 ip                    struct sockaddr_in {               sa_family_t    sin_family; address family: AF_INET                in_port_t      sin_port;    port in network byte order               struct in_addr sin_addr;   internet address           };           Internet address.            struct in_addr {               uint32_t       s_addr;     address in network byte order            };        */        printf("socket create success!\n");        struct sockaddr_in myaddr;        myaddr.sin_family = AF_INET;        myaddr.sin_port = htons(8080);// port in network byte order        myaddr.sin_addr.s_addr = inet_addr("127.0.0.1");// internet address     // const struct sockaddr addr = (struct sockaddr *)&myaddr;      socklen_t addrlen = sizeof(myaddr);        int result =  bind(socketFd, (struct sockaddr*)&myaddr, addrlen);        if (result == 0){            printf("bind success!\n");              // listen()  marks  the  socket referred to by sockfd as a passive socket            //  that is, as a socket that will be used to accept incoming connection requests using accept(2)            //  int listen(int sockfd, int backlog);            //  The backlog argument defines the maximum length to which the queue of pending connec-     //  tions for sockfd may grow. SOMAXCONN  with the value 128.            // 监听的时候的sockfd是一个被动套接字,被动套接字是用来接受连接的,只能用来用来监听            result = listen(socketFd,SOMAXCONN);            if (result == 0){                printf("listening...!\n");                  //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);                //accept,会创建一个新的连接,这是一个主动套接字,可以读写,与客户端会话                    // 所以服务器端至少有两个套接字                struct sockaddr_in peeraddr;// 对方的tcpip地址结构                socklen_t addrlen = sizeof(peeraddr);            //The argument addr is a pointer to a sockaddr structure.  This structure is filled  in       //   with the address of the peer socket, as known to the communications layer                unsigned int conn = accept(socketFd,(struct sockaddr *)&peeraddr,&addrlen);                if (conn > 0){                    printf("accept success!\n");                    char recv_buf[1024] = {0};                    while(1){                        // 读取客户端发送过来的数据                        // ssize_t read()(int fd, void *buf, size_t count);                        result = read(conn,recv_buf,sizeof(recv_buf));                        if (result == 0){                            // zero indicates end of file                        }else if(result == -1){                            // error                        }                        // 读取数据成功,输出接受的内容                        fputs(recv_buf,stdout);                        // 向客户端回数据                        // ssize_t write(int fd, const void *buf, size_t count);                        char *send_buf = "from Server : data had recevied,thanks!";                        result = write(conn,send_buf,strlen(send_buf));                        if (result == 0){                            printf("nothing was writen to client!\n");                        }else if (result == -1){                            printf("writen to client error!\n");                        }else{                            printf("writen to client success!\n");                        }                    }                }            }        }    }}

Client.c

#include <sys/types.h> #include <sys/socket.h>#include <stdio.h>#include <netinet/in.h>#include <netinet/ip.h> void main(){    /*     domain : protocol  family  which will be used for communication    type   :  specifies the communication semantics 指明通信的语义     int socket(int domain, int type, int protocol);     */    int domain = AF_INET;    //  Provides sequenced, reliable, two-way, connection-based byte  streams.    int type = SOCK_STREAM;    int protocol = 0;    int socketFd = socket(domain,type,protocol);    if (socketFd != -1){        /* socket创建成功,bind        bind需要的是一个通用的地址结构,我们给定一个具体的ipv4的地址结构                    man 7 ip                    struct sockaddr_in {               sa_family_t    sin_family; address family: AF_INET                in_port_t      sin_port;    port in network byte order               struct in_addr sin_addr;   internet address           };           Internet address.            struct in_addr {               uint32_t       s_addr;     address in network byte order            };        */        printf("socket create success!\n");        struct sockaddr_in myaddr;        myaddr.sin_family = AF_INET;        myaddr.sin_port = htons(8080);// port in network byte order        myaddr.sin_addr.s_addr = inet_addr("127.0.0.1");// internet address     // const struct sockaddr addr = (struct sockaddr *)&myaddr;      socklen_t addrlen = sizeof(myaddr);        int result = connect(socketFd, (struct sockaddr *)&myaddr,addrlen);        if(result == 0){             char send_buf[1024] = {0};             char recv_buf[1024] = {0};             while(fgets(send_buf,sizeof(send_buf),stdin)!=NULL){                    result = write(socketFd,send_buf,strlen(send_buf));                    if (result == 0){                        printf("nothing was writen to server!\n");                    }if (result == -1){                        printf("writen to server error!\n");                    }else{                        printf("writen to server success!\n");                        result = read(socketFd,recv_buf,sizeof(recv_buf));                        if (result == 0){                            // zero indicates end of file                        }else if(result == -1){                            // error                        }                        // 读取数据成功,输出接受的内容                        fputs(recv_buf,stdout);                    }                    memset(recv_buf,0,sizeof(recv_buf));                    memset(send_buf,0,sizeof(send_buf));             }        }     }}

终于写完了这篇文章了,当然也参考了网上的资料,在此对网络上的学习资料共享者表示感谢。
本文出自andywuchuanlong的CSDN博客,转载请说明出处,谢谢!

2 0