Linux - 网络编程之TCP通讯

一切皆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()函数创建了一个通信的切入点,并且返回一个描述符,而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传输协议。 (不指定可以填 0 )
这样创建了一个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的协议族地址结构如下:(这里只介绍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");
    }
}
结果:我电脑上的结果是:小端模式。

当我们知道主机的字节序类型之后,如何把主机字节序转化成网络字节序呢?这个时候可以参考我们的下列转化函数:

//  h表示主机序  to  n表示网络序 s l 表示类型
  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);  //将字符串转换为32位二进制网络字节序的IPV4地址
    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)backlog参数: 定义了正在排队等待连接的socket描述符的最大个数,SOMAXCONN 表示 128。

4. accept()
用于接受客户端的请求,建立两者之间的连接。

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

a)sockfd: 是刚才创建的被动套接字的描述符。
b)addr: 代表的是客户端的协议地址。
c)addrlen: 协议地址的长度。
如果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变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。
大于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(){
    int domain = AF_INET;      //ipv4协议
    int type = SOCK_STREAM;    //流式Socket(SOCK_STREAM)是一种面向连接的Socket
    int protocol = 0;          //不指定具体协议可以赋值 0
    int socketFd = socket(domain,type,protocol); //创建socket通讯

    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);   // 进程通讯端口
//        myaddr.sin_addr.s_addr = inet_addr("127.0.0.1");// 将要绑定的本机IP地址,转换成网络字节序(inet_addr输入的是字符串)
        myaddr.sin_addr.s_addr = htonl(INADDR_ANY);  //INADDR_ANY自动获得本机IP,再将转换成网络字节序(获取的是unsigned long )

     // 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");  
            // 监听的时候的sockfd是一个被动套接字,被动套接字是用来接受连接的,只能用来用来监听
            result = listen(socketFd,SOMAXCONN); //正在排队等待连接的socket描述符的最大个数,SOMAXCONN 表示 128。 
            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);
                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){
  // 小于0表示出现了错误。如果是ECONNREST表示网络连接出了问题(对方已经关闭了连接),此时可以退出。                       
                            // 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");
 // 小于0表示出现了错误。如果是ECONNREST表示网络连接出了问题(对方已经关闭了连接),此时可以退出。
                        }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(){
    int domain = AF_INET;    //ipv4协议
    int type = SOCK_STREAM;  //流式Socket(SOCK_STREAM)是一种面向连接的Socket
    int protocol = 0;
    int socketFd = socket(domain,type,protocol);

    if (socketFd != -1){
        /* socket创建成功,bind
        bind需要的是一个通用的地址结构,我们给定一个具体的ipv4的地址结构
        */
        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
        
//也可以采用这种运行demo的时候在后面手动输入IP和端口号,主函数要修改int main(int argc, char const *argv[])
//       srvaddr.sin_port = htons(atoi(argv[2]));
//       inet_pton(AF_INET, argv[1], &srvaddr.sin_addr);
	 
     // 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");
 // 小于0表示出现了错误。如果是ECONNREST表示网络连接出了问题(对方已经关闭了连接),此时可以退出。                        
                    }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));
             }
        } 
    }
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值