C++网络编程(一):TCP套接字编程

目录

    • 基本数据结构
    • TCP服务器端的默认函数调用顺序
    • TCP客户端的默认函数调用情况
    • TCP网络编程主要流程
    • TCP客户端套接字的地址分配
    • TCP套接字的I/O缓存
    • 代码实例
    • 面试常见问题详解
    • 参考资料

基本数据结构

具体可见:sockaddr和sockaddr_in详解

套接字的普通定义通用的地址结构:

struct sockaddr
structsockaddr {

u_charsa_len;//长度

u_short  sa_family;//协议

char  sa_data[14];//数据

};

IP专用的地址结构:

struct sockaddr_in

struct sockaddr_in {

u_char    sin_len;//长度

u_short   sin_family;//协议

u_short    sin_port;//端口

struct in_addr   sin_addr;//ip地址

char   sin_zero[8];//数据

};

结构体in_addr见下图:

C++网络编程(一):TCP套接字编程_第1张图片


表示一个32位的IPv4地址结构:

struct in_addr

structin_addr {

           u_longs_addr;

};

TCP服务器端的默认函数调用顺序

C++网络编程(一):TCP套接字编程_第2张图片

int Socket( int domain, int type,int protocol)

功能:创建一个新的套接字,返回套接字描述符

参数说明:

domain:域类型,指明使用的协议栈,如TCP/IP使用的是 PF_INET    ,其他还有AF_INET6、AF_UNIX

type:指明需要的服务类型, 如

SOCK_DGRAM:数据报服务,UDP协议

SOCK_STREAM:流服务,TCP协议

protocol:一般都取0(由系统根据服务类型选择默认的协议)
int bind(int sockfd,struct sockaddr* my_addr,int addrlen) 

功能:为套接字指明一个本地端点地址

TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接

参数说明:

sockfd:套接字描述符,指明创建连接的套接字

my_addr:本地地址,IP地址和端口号

addrlen:地址长度

套接口中port=0表示由内核指定端口号,设定sin_addr为INADDR_ANY,由内核指定IP地址。
int listen(int sockfd,intinput_queue_size)

功能:

面向连接的套接字使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的

参数说明:

Sockfd:套接字描述符,指明创建连接的套接字

input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数
int accept(int sockfd, structsockaddr *addr, int *addrlen)

功能:获取传入连接请求,返回新的连接的套接字描述符。

为每个新的连接请求创建了一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接收其他的连接请求。新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。

参数说明:

Sockfd:套接字描述符,指明正在监听的套接字

addr:提出连接请求的主机地址

addrlen:地址长度
int send(int sockfd, const void * data, int data_len, unsigned int flags)

功能:

在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1。send会将外发数据复制到OS内核中,也可以使用send发送面向连接的UDP报文。

参数说明:

sockfd:套接字描述符

data:指向要发送数据的指针

data_len:数据长度

flags:通常为0

如果send()函数的返回值小于len的话,则你需要再次发送剩下的数据。802.3,MTU为1492B,如果包小于1K,那么send()一般都会一次发送光的。
int recv(int sockfd, void *buf, intbuf_len,unsigned int flags)

功能:

从TCP接收数据,返回实际接收的数据长度,出错时返回-1。

服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞。如果TCP收到的数据大于(/小于)缓存的大小,只抽出能够填满缓存的足够数据(/抽出所有数据并返回它实际接收的字节数)。也可以使用recv接收面向连接的UDP的报文,若缓存不能装下整个报文,填满缓存后剩下的数据将被丢弃。

参数说明:

Sockfd:套接字描述符

Buf:指向内存块的指针

Buf_len:内存块大小,以字节为单位

flags:一般为0(MSG_WAITALL接收到指定长度数据时才返回),设置为 MSG_DONTWAIT为非阻塞
close(int sockfd)

功能:

撤销套接字.如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则关闭连接并撤销套接字。

参数说明:

sockfd:套接字描述符

TCP客户端的默认函数调用情况

C++网络编程(一):TCP套接字编程_第3张图片

socket、send、recv、close函数的用法同服务器端。

int connect(int sockfd,structsockaddr *server_addr,int sockaddr_len)

功能: 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。

参数说明:

Sockfd:套接字描述符,指明创建连接的套接字

Server_addr:指明远程端点:IP地址和端口号

sockaddr_len :地址长度

write、read和send、recv的区别可见此博客

TCP网络编程主要流程

C++网络编程(一):TCP套接字编程_第4张图片

TCP客户端套接字的地址分配

在上面的图中,客户端中并没有调用bind函数分配套接字地址,这是为什么呢?

因为客户端套接字在调用connect函数时,会让操作系统内核完成了对IP、端口号的自动分配,其中IP用主机IP,端口在临时端口号范围内随机分配。


TCP套接字的I/O缓存

TCP套接字的数据收发无边界。服务器端即使调用1次write函数传输40字节数据,客户端也有可能通过4次read函数每次读取10字节,这是为什么呢?

我们都知道TCP是面向字节流的传输,设有发送缓冲区和接收缓冲区。实际上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。write函数调用瞬间,数据将移至发送缓冲区;read函数调用瞬间,从接收缓冲区读取数据。

调用write后,数据移至发送缓冲区,在适当的时候传向对方的接收缓冲区,对方调用read函数从接收缓冲区读取数据。

代码实例

以下代码基于linux环境。

服务器等待客户端连接,连接后等待客户端发送数据,服务器收到客户端发来的数据之后再发送回去,一个“回发”的功能。

服务器端:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
#define MYPORT  8887
#define QUEUE   20
#define BUFFER_SIZE 1024
 
int main()
{
    ///定义sockfd
    int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
    
    ///定义sockaddr_in
    struct sockaddr_in server_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(MYPORT);
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    ///bind,成功返回0,出错返回-1
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1)
    {
        perror("bind");
        exit(1);
    }
    
    printf("监听%d端口\n",MYPORT);
    ///listen,成功返回0,出错返回-1
    if(listen(server_sockfd,QUEUE) == -1)
    {
        perror("listen");
        exit(1);
    }
    
    ///客户端套接字
    char buffer[BUFFER_SIZE];
    struct sockaddr_in client_addr;
    socklen_t length = sizeof(client_addr);
    
    printf("等待客户端连接\n");
    ///成功返回非负描述字,出错返回-1
    int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
    if(conn<0)
    {
        perror("connect");
        exit(1);
    }
    printf("客户端成功连接\n");
    
    while(1)
    {
        memset(buffer,0,sizeof(buffer));
        int len = recv(conn, buffer, sizeof(buffer),0);
        //客户端发送exit或者异常结束时,退出
        if(strcmp(buffer,"exit\n")==0 || len<=0)
            break;
        printf("来自客户端数据:%s\n",buffer);
        send(conn, buffer, len, 0);
        printf("发送给客户端数据:%s\n",buffer);
    }
    close(conn);
    close(server_sockfd);
    return 0;
}

客户端:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
#define MYPORT  8887
#define BUFFER_SIZE 1024
char* SERVER_IP = "127.0.0.1";
 
int main()
{
    ///定义sockfd
    int sock_cli = socket(AF_INET,SOCK_STREAM, 0);
    
    ///定义sockaddr_in
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(MYPORT);  ///服务器端口
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);  ///服务器ip
    
    printf("连接%s:%d\n",SERVER_IP,MYPORT);
    ///连接服务器,成功返回0,错误返回-1
    if (connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("connect");
        exit(1);
    }
    printf("服务器连接成功\n");
    char sendbuf[BUFFER_SIZE];
    char recvbuf[BUFFER_SIZE];
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        printf("向服务器发送数据:%s\n",sendbuf);
        send(sock_cli, sendbuf, strlen(sendbuf),0); ///发送
        if(strcmp(sendbuf,"exit\n")==0)
            break;
        recv(sock_cli, recvbuf, sizeof(recvbuf),0); ///接收
        printf("从服务器接收数据:%s\n",recvbuf);
        
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }
    
    close(sock_cli);
    return 0;
}

运行结果如下:

C++网络编程(一):TCP套接字编程_第5张图片

面试常见问题详解

  1. connect函数的作用?

    答:客户端的connect() 函数功能是让客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果作为返回值(成功连接为0, 失败为-1)。

  2. listen函数的作用?

    答:listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度。

  3. 内核为任何一个给定的监听套接口维护哪两个队列?

    答:未完成连接队列和已完成连接队列。

    当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的 SYN 响应,其中稍带对客户 SYN 的 ACK(即SYN+ACK),这一项一直保留在未完成连接队列中,直到三次握手的第三个分节(客户对服务器 SYN 的 ACK )到达或者该项超时为止。

    如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。

  4. 怎么理解listen函数的backlog参数?

    答:backlog 参数被定义为未完成连接队列和已完成连接队列大小之和,大多数实现默认值为 5,当服务器把已完成连接队列的某个连接取走后,这个队列的位置又空出一个,这样来回实现动态平衡。


  1. accept函数的作用?

    答:从已完成连接队列头部取出一个已经完成的连接,得到一个新的套接字,使用这个新套接字与客户端进行数据传输。如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。

  2. connect和accept的调用发生在三次握手的哪个阶段?

    答:这个问题其实本身就有点问题,因为确切的说是三次握手发生在connect函数调用时,connect的作用是让Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果作为返回值。

    如果硬要回答的话,可以说:三次握手发生在connect阶段。

    而accept的作用是从已完成连接队列头部取出一个已经完成的连接,可以说是在三次握手成功建立连接之后发生的。

  3. connect成功之后,accept返回之前,客户端和服务器的连接是否已建立?

    答:是。connect会让Linux 内核自动完成 TCP 三次握手连接,因此connect成功意味着三次握手成功,连接已经建立;accept的作用就是从已连接队列中取出优先级最高的一个连接,并将它绑定给一个新的套接字,与连接是否建立没有直接关系。

  4. connect函数和accept函数哪个先返回? 客户端和服务端哪个先完成建立tcp连接?

    答:connect函数先返回,因为其让内核完成三次握手建立连接。客户端先完成建立tcp连接,因为connect正常返回后客户端认为建立了TCP连接, accept函数正常返回后服务端认为建立了TCP连接。

  5. 如果服务器没有调用accept,connect能调用成功吗?客户端是否还可以通信?

    答:能调用成功,accept只是从已完成连接队列取出连接而已,connect是让内核三次握手,二者相互独立,不调用accept完全不影响connect。客户端可以发数据给服务器,但服务器不调用accept就无法得到连接对应的新套接字,因此不能发数据给客户端,也收不到客户端发来的信息。


  1. 为什么TCP服务端需要调用bind函数而客户端通常不需要呢?

    答:因为bind的作用是为套接字指定ip和端口号,而客户端的端口号(临时端口号)一般由操作系统随机指定,不需要手动分配,如果自己手动分配有可能会用了熟知端口号或者注册端口号,带来冲突。


参考资料

  • 《TCP IP网络编程》尹圣雨
  • https://blog.csdn.net/lell3538/article/details/53335231
  • https://blog.csdn.net/tennysonsky/article/details/45621341
  • https://blog.csdn.net/wwxy1995/article/details/108325337
  • https://blog.csdn.net/stpeace/article/details/74035204
  • https://blog.csdn.net/liu35937266/article/details/56494348
  • https://blog.csdn.net/weixin_43918473/article/details/105302335
  • https://blog.csdn.net/stpeace/article/details/45001255?biz_id=102&utm_term=%E4%B8%BA%E4%BB%80%E4%B9%88%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%8D%E7%94%A8bind&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-45001255&spm=1018.2118.3001.4187

你可能感兴趣的:(网络,内核,队列,socket)