【网络基础-socket套接口编程-TCP】面试一线大厂后才知道,面试官都希望你掌握这些套接口编程知识(bind,listen,connect,socket,accept)

首先对于访问上篇博文(【网络基础-计算机字节序】)的同学们表示感谢,对于第一篇博文就有1k左右的浏览量“零号”还是很开心的。以后也会争取给大家发布更有价值的,通俗易懂的博文。“零号‘’是开心了,但是“壹号”同学就没有这么顺利,他又面试失利了,他上篇博文搞定了“网络套接字”的问题,但这次又败在了对TCP套接口编程的理解上,此篇博文我们就说一说TCP套接口编程到底有哪些难搞的地方,有哪些坑容易被问到。照例,先列出失败的问题:

面试官问题1:TCP客户端与服务器端的创建过程您了解吗?

面试官问题2:可以谈谈您对于每个函数的理解吗?

面试官问题3:可以说一说您对listen以及accept函数的理解吗?

首先,先来了解一下TCP套接口编程的过程。如图1(图片来自UNIX网络编程-卷一)

                                                                      图1

从上图可以看出,客户与服务器的通信流程为:

服务器端:

  • 调用socket创建套接字描述字
  • 调用bind绑定服务ip和端口号
  • 调用listen将主动连接套接字改为被动连接套接字
  • 调用accept等待客户端连接
    • 有客户连接后:
      • read读取客户发送来的数据
      • 处理用户请求
      • write给客户发送回复信息
      • 处理完成后,调用close关闭accept返回的已连接套接字

客户端:

  • 调用socket创建套接字描述字
  • 调用connect请求连接服务器
    • 连接成功后:
      • write发送请求
      • read接收服务器的应答
      • 进行业务处理
      • 处理完成后,调用close关闭socket创建的套接字描述字

服务器端在通信过程中执行的接口分别为,socket,bind,listen,accept,read,write,close;客户端在通信过程中分别调用的接口为socket,connect,write,read,close。

下面我们来详细说一说通信过程中用到的这些接口及其功能:

socket:创建套接口描述字(套接字)

头文件:#include <sys/socket.h>

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

返回值:成功:非负描述字,失败:-1
参数说明:

family:协议簇选项,当前套接口的协议类型。图2解释了不同值代表的意义

type:常用类型,TCP一般为字节流套接口,UDP一般为数据报套接口,如下图3

protocol:协议号,通常设置为0,但在原始套接口上可能会有不同。

 

                                    图2

                                       图3

family&type组合成的协议类型如下图4,从图中我们可以看出TCP都是字节流型的报文,UDP都是数据报类型的报文

                                         图4

 

connect:客户端用来建立与服务器的连接

头文件:#include <sys/socket.h>

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

返回值:成功:0 错误:-1
参数说明:

sockfd:套接口描述字,客户端socket函数创建的套接字

servaddr:套接口地址结构指针,其内部必须包含服务器的IP地址和端口号

addrlen:套接口地址结构大小

延伸:在TCP连接过程中,connect函数激发TCP的三路握手过程,且仅在连接建立成功或出错时才返回,返回的错误可能有以下几种情况:

  1. tcp客户没有收到SYN分节的响应,则返回ETIMEDOUT。
  2. 客户接受到RST响应,则表明该服务器主机在我们指定的端口上没有进程与之连接。
  3. 如果某客户发出的SYN在中间的路由器上引发了一个目的地不可达ICMP错误。客户保存此消息,并按一定的时间间隔连续发出SYN。若在规定时间后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSHTUNREACH或ENETUNREACH错误返回给进程。

注:如果connect函数返回失败,则套接口不可再用,必须关闭,不能再对此套接口再调用connect函数去连接服务器。

 

bind:将套接字绑定一个IP地址和端口号

#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr * myaddr,socklen_t addrlen);
返回:成功:0 出错:-1
参数说明:
sockfd:套接口描述字,客户端socket函数创建的套接字

servaddr:套接口地址结构指针,内部设置的是将要给sockfd绑定的ip以及端口号

addrlen:套接口地址结构大小

延伸:对于TCP,调用bind可以指定一个端口号,指定一个IP地址,可以两者都指定,也可以一个也不指定。当服务器启动时,要通过bind捆绑一个端口,如果TCP客户或服务器不这么做,当调用函数connect或listen时,内核就要为套接口选则一个临时端口。大多数TCP客户都是这么做的。但是对于TCP服务来说是及其少见的,因为客户连接服务器需要知道服务器的端口

进程可以把一个特定的IP地址捆绑到它的套接口上。客户和服务器绑定情况如下:

  • 如果参数servaddr指定了ip和端口号,就是为参数sockfd这个套接字绑定ip和端口号。
  • 客户端调用bind:客户在与服务器通信时,报文中源IP与源端口号就是客户端调用bind绑定的地址。
  • 服务器调用bind:对于多ip服务器如果绑定的是INADDR_ANY那么对于服务器所有ip的访问都可以被支持,如果只绑定了服务器的IP A,则只有对A ip及绑定端口号的服务会被支持。

注:如果TCP服务器不把IP地址捆绑到套接口上,内核就把客户所发SYN所在分组的目的IP地址作为服务器的源IP地址。(效果跟绑定通配地址符相同

对于服务器调用举个粟子:

    一台服务器有两个ip,ip1:192.10.1.110、ip2:192.10.1.111,在创建socket服务时分为下列几种情况:

  1. bind绑定的是INADDR_ANY(通配地址符),那么客户连接该服务时,通过ip1或ip2都可以连接成功。
  2. bind绑定了其中一个ip(假设为ip1),那么客户端只能通过绑定ip(ip1)进行访问,所有通过其它ip(ip2)访问的将连接失败。
  3. 没有调用bind函数,会由内核选择端口号,可以通过getsockname函数返回。

bind函数设置ip和端口号的组合,导致的结果,如下图5:

                                                   图5

:如果让内核来为套接口选择端口号,函数bind并不返回所选择的值。为了得到内核所选择的这个临时端口值,必须调用函数getsockname来返回协议地址(getsockname函数后面会介绍)。

常见错误:EADDRINUSE(地址已使用,当前绑定ip及port已经被占用且没有设置SO_REUSEADDR和SO_REUSEPORT属性)

listen:将主动套接口转换为被动套接口

#include<sys/socket.h>
int listen(int sockfd,int backlog);

返回值:成功:0 出错:-1

参数说明:
socket:套接字描述字
backlog:内核为此套接口排队的最大连接个数(已连接队列,未连接队列两个队列总和的最大值)。

说明:当函数socket创建一个套接口时,它被假设为一个主动套接口,也就是说,它是一个将调用connect发起连接的客户套接口,函数listen将未连接的套接口状态转换成被动套接口,指示内核应接受指向此套接口的连接请求。根据TCP套接口转换图,调用Listen导致套接口从CLOSED状态转换到LISTEN状态。

延伸:对于给定监听套接口,内核要维护两个队列

  1. 未完成连接队列,为每个这样的SYN分节开设一个条目:已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手连接。这些套接口都处于SYN_RCVD状态。
  2. 已完成连接队列,为每个已完成TCP三路握手过程的客户开设一个条目。这些套接口都处于ESTABLISHED状态。

下图是这两个在TCP连接过程中的图示(图6:TCP监听套接口维护的两个队列;图7:TCP三路握手和监听套接口的两个队列交互过程):

                                                   图6

                                                    图7

注:当一个客户SYN到达时,若两个队列都是满的,TCP就忽略此分节,且不发送RST。因为这种情况是暂时的,客户TCP将重发SYN,期望不久就能在队列中找到空闲条目。要是TCP服务器发送了一个RST,客户的connect函数将立即返回一个错误,强制应用进程处理这种情况,而不是让TCP正常的重传机制来处理。

accept:接受客户端的连接,返回已连接套接字

#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);

返回值:成功:非负描述字 出错:-1

参数说明:
sockfd:监听套接口描述字(一个给定的服务器常常是只生成一个监听套接口且一直存在,直到服务器关闭)

cliaddr:发起连接请求的客户端网络套接字结构地址,如果不在意可以设为NULL

addrlen:网络套接字地址结构长度

返回值说明:已连接套接口描述子(内核为每个被接受的客户连接创建了一个已连接套接口。当服务器完成某客户的服务时,关闭已连接套接口)。该函数最多返回三个值,一个即可能是新套接口描述字也可能是错误指示的整数,一个客户进程的协议地址(由指针cliaddr所指)以及该地址的大小(由指针addrlen所指)。

accept执行成功返回的是内核自动生成一个全新的描述字,代表与客户TCP连接。

close:关闭套接字

#include <unistd.h>
int close(int sockfd);
返回值:成功:0 出错:-1

功能:将套接口做上“已关闭”标记,并立即返回到进程。这个套接字不能在为进程所用:它不能被read或write操作。但TCP将试图发送已排队待发的任何数据,然后按正常的TCP连接终止序列进行操作。

描述字访问计数:

当调用close对套接字进行关闭时,close只是将其参数对应的套接字的引用计数减一,如果引用计数小于等于0,则会引发四分组终止序列。父进程通过fork创建一个子进程,这时候父进程对应的网络套接字的引用计数会+1。

延伸:如果我们想对一个TCP发送FIN,应该用函数shutdown

通过上面的介绍,大家可以写出一个TCP客户与服务器的通信实例吗?建议大家自己动手试一下:

延展:

延展一:getsockname和getpeername函数:返回与套接口关联的本地协议地址或远程协议地址

#include <sys/socket.h>

int getsockname(int sockfd,struct sockaddr* localaddr,socklen_t *addrlen);

int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen);

返回值:成功:0 失败:-1

功能:可以通过套接字描述符来获取自己的IP地址和连接对端的IP地址

用到这两个函数的场景:

  • 在一个不调用bind的TCP客户上,当connect成功返回后,getsockname返回内核分配给此连接的本地IP地址和本地端口号。
  • 再以端口号0调用bind后(通知内核选择本地端口号),getsockname返回由内核分配的本地端口号。
  • getsockname可用来获取某个套接口的地址族。
  • 在捆绑了一个通配IP地址的TCP服务器上,一旦与客户建立了连接(accept成功返回),就可以调用getsockname来获得分配给此连接的本地IP地址。在这样的调用中套接口描述字必须是已连接套接口描述字,而不是监听套接口的描述字。
  • 在服务器端accept成功后,通过getpeername()函数来获取当前连接的客户端的IP地址和端口号。

延展2:计算机字节序

【网络基础-计算机字节序】

我在这举个最简单的TCP客户端与服务器端实例:

功能:服务器接收客户端发过来的请求消息,建立连接,打印出客户端的ip以及端口号,然后接收客户端发送过来的消息并打印,在原消息前新增hello后发回给客户,客户端打印。注:我这个示例是可以直接运行的,里面用到了select,fork等本文没讲的接口,但后期也会讲。感兴趣的可以先自己搜一下,或者关注我,这样后面更新可以及时看到。

结果展示:

服务端:

客户端:

源码:

util.h

#ifndef __UTIL__
#define __UTIL__
#include <stdio.h>

extern const uint16_t server_port;//端口号使用1024~49151之间
extern const uint16_t listen_num;
extern const uint16_t max_len;
//信号处理函数
void SIGPIPEHandler(int nSig);
void SIGCHLDHandler(int nSig);

//socket包裹函数
int Socket(int family,int type,int protocol);

int Fork();
int doit(int conn_fd,void *_client_addr);
void str_cli(FILE *fp,int sockfd);
#endif

util.c

#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>

#include "util.h"

const uint16_t server_port = 6666;//端口号使用1024~49151之间
const uint16_t listen_num = 10;
const uint16_t max_len = 256;

#define min(x, y) ({ \
      typeof(x) _min1 = x; \
      typeof(y) _min2 = y; \
      (void) (&_min1 == &_min2); \
      _min1 < _min2 ? _min1 : _min2; })
  
#define max(x, y) ({ \
      typeof(x) _max1 = x; \
      typeof(y) _max2 = y; \
     (void) (&_max1 == &_max2); \
     _max1 > _max2 ? _max1 : _max2; })

void SIGPIPEHandler(int nSig)
{
    printf("capture a SIGPIPE signal %d\n", nSig);
}

//等待回收子进程,防止生成僵尸进程
void SIGCHLDHandler(int nSig)
{
    pid_t pid;
    int   stat;
    while((pid = waitpid(-1,&stat,WNOHANG))>0)
    {
        printf("child %d terminated\n",pid);
    }
    return ;
}

//创建ipv4 字节流套接字
int Socket(int family,int type,int protocol)
{
    int sockfd;
    if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)//0 right -1 error
    {
        fprintf(stderr, "socket fail value of erron:%d Error message: %s\n", errno,strerror(errno));
        return -1;
    }
    return sockfd;
}

int Fork()
{
    pid_t pid;
    pid = fork();
    if(pid == -1)
    {
        printf("fork fail errno : %d Error message:%s\n",\
        errno,strerror(errno));
        return -1;
    }
    return pid;
}

int doit(int conn_fd,void *_client_addr)
{
    struct sockaddr_in * client_addr = (struct sockaddr_in *)_client_addr; 
    char cli_ip[max_len];
    char buf[max_len];
    char rbuf[max_len];
    int n_write;
    inet_ntop(AF_INET,(void *)&client_addr->sin_addr,(void *)cli_ip,sizeof(cli_ip));
    printf("connect with client ip:%s port:%u\n",cli_ip,ntohs(client_addr->sin_port));
    while(1)
    {
    //接收客户端发送过来的信息,并将接收到的数据添加信息后发回给数据
        if(read(conn_fd,buf,sizeof(buf))<=0)
        {
            printf("read num <=0\n");
        }
        printf("recv buf : %s\n",buf);
        strcpy(rbuf,"hello ");
        strcat(rbuf,buf);
        if((n_write = write(conn_fd,rbuf,strlen(rbuf)))<=0)
        {
            printf("write num <=0\n");
        }
        else
        {
            printf("write num = %d\n",n_write);
        }
        memset(buf,0,sizeof(buf));
        memset(rbuf,0,sizeof(rbuf));
    }
    return 1;
}
void str_cli(FILE *fp,int sockfd)
{
    int maxfdp1,stdlneof;
    int n_read;
    fd_set rset;
    char buf[max_len]; 
    char rbuf[max_len];  
    stdlneof = 0;
    int sel_ret;
    int sock_use = 1;//用于判断sock套接字是否可用
    struct timeval timeout={0,3};
    for(;;)
    {
        FD_ZERO(&rset);//select每次调用后都要清空重置,否则不能检测描述符变化
        if(sock_use)
        {
            FD_SET(sockfd,&rset);//如果套接字在本端已经关闭,继续添加近select会报错,因为套接字已经不存在了
            FD_SET(fileno(fp),&rset);
        }
        maxfdp1 = max(fileno(fp),sockfd) +1;
        if((sel_ret = select(maxfdp1,&rset,NULL,NULL,(struct timeval *)&timeout))==-1)
        {
            printf("select failed errno:%d Error message:%s\n",errno,strerror(errno));
            continue;
        }
        else if(sel_ret)
        {

            printf("sel_ret %d\n",sel_ret);
            if(FD_ISSET(sockfd,&rset))
            {
               if((n_read = read(sockfd,rbuf,sizeof(rbuf)))<0)
               {
                  printf("read failed errno:%d Error message:%s\n",errno,strerror(errno));
               }
               else if(n_read == 0)
               {
                   //在这里,如果read返回0,那么该套接字已经关闭应该返回。
                   close(sockfd);
                   sock_use = 0;
                   //FD_CLR(sockfd,&rset);
               }
               printf("recv len : %d buf : %s\n",n_read,rbuf);
            }

        
            if(FD_ISSET(fileno(fp),&rset))
            {
                if(fgets(buf,sizeof(buf),fp)!=NULL)
                {
                    //stdlneof = 1;
                    int n_write;
                    if((n_write = write(sockfd,buf,strlen(buf)))<=0)
                    {
                        printf("write num <=0\n");
                    }
                    else
                    {
                        printf("write num = %d\n",n_write);
                    }
                }
            }
        }
        else
        {
            continue;
        }
        sleep(2);
        memset(buf,0,sizeof(buf));
        memset(rbuf,0,sizeof(rbuf));
    }
}

服务器源文件 server.c

/*
1. socket创建字节流套接字
2. bind绑定ip,port
3. listen从主动套接口,转换为监听套接口
4. accept阻塞等待客户链接
5. 通过已连接套接字,获取客户端IP端口号
6. read获取客户发送过来的数据
7. 添加hello write给客户端
*/

#include <stdio.h>
#include <stdint.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>//fork函数头文件
#include <stdlib.h>
#include <signal.h>

#include "util.h"

extern int errno;


int main(int argc,char **argv)
{
    //1. 声明ipv4套接字地址结构变量
    struct sockaddr_in server_addr,client_addr;
    socklen_t addr_len = sizeof(struct sockaddr_in);
    bzero(&server_addr,sizeof(struct sockaddr_in));
    bzero(&client_addr,sizeof(struct sockaddr_in));
    int sockfd,conn_fd;
    //网络地址结构中端口和ip都要用网络序进行存储
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons((uint16_t)server_port);
    //server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //创建ipv4 字节流套接字
    if((sockfd = Socket(AF_INET,SOCK_STREAM,0)) == -1)//0 right -1 error
    {
        return -1;
    }

    if(bind(sockfd,(const struct sockaddr *)&server_addr,addr_len) == -1)//0 right -1 error
    {
        fprintf(stderr, "bind fail value of erron:%d Error message: %s\n", errno,strerror(errno));
        return -1;
    }

    if(listen(sockfd,listen_num)== -1)//0 right -1 error
    {
        fprintf(stderr, "listen fail value of erron:%d Error message: %s\n", errno,strerror(errno));
        return -1;
    }

    //接收SIGCHLD信号
    signal(SIGCHLD,SIGCHLDHandler);
    pid_t pid;

    while(1)
    {
        conn_fd = accept(sockfd,(struct sockaddr *)&client_addr,&addr_len);
        if(conn_fd == -1)
        {
            fprintf(stderr, "accept fail value of erron:%d Error message: %s\n", errno,strerror(errno));
            continue;
        }
        if((pid = Fork()) == 0)
        {
            close(sockfd);//在子进程中关闭监听套接口
            doit(conn_fd,&client_addr);//调用处理函数
            close(conn_fd);//关闭已连接套接口
            exit(0);
        }
        close(conn_fd);
    }
    return 0;
}

客户端源文件 client.c

#include <stdio.h>
#include <stdint.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <signal.h>

#include "util.h"


int main(int argc,char **argv)
{
    if(argc != 2)
    {
        printf("参数错误,请重新执行(参数应为服务器ip)\n");
        return -1;
    }

    //接收处理RST信号
    signal(SIGPIPE,SIGPIPEHandler);
    int sockfd;
    if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        printf("socket failed errno:%d Error message:%s \n",errno,strerror(errno));
        return -1;
    }

    struct sockaddr_in conn_addr;
    bzero((void *)&conn_addr,sizeof(conn_addr));
    conn_addr.sin_family = AF_INET;
    conn_addr.sin_port = htons(server_port);

    if(inet_pton(AF_INET,(const char *)argv[1],(void *)&conn_addr.sin_addr)== 0)
    {
        printf("inet_pton failed errno:%d Error message:%s\n",errno,strerror(errno));
        return -1;
    }

    if(connect(sockfd,(struct sockaddr *)&conn_addr,(socklen_t)sizeof(conn_addr))==-1)
    {
        printf("connect failed errno:%d Error message:%s\n",errno,strerror(errno));
        return -1;
    }
    struct timeval timeout = {3,0};
    setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,(const char *)&timeout,sizeof(timeout));
    //FILE *fp = open("hh.txt",O_CREATE)
    str_cli(stdin,sockfd);

    return 0;
}
发布了4 篇原创文章 · 获赞 7 · 访问量 1022

猜你喜欢

转载自blog.csdn.net/JianChiBieFei/article/details/105387242
今日推荐