网络编程-接口方法

189 阅读3分钟

介绍

本节主要介绍网络编程的理论,从服务端/客户端示例来解析网络编程常用的方式,使用到的一些方法,其中包括:socket、connect、bind、listen、accept、close。

图1是client与server间交互最常见的一种场景:

  1. server启动,绑定指定端口,开始监听
  2. client启动并连接上server
  3. client向server发送消息
  4. server端接收到消息进行处理
  5. server返回响应给client端
  6. client接收消息
  7. client若继续交互,则重复3-6步骤
  8. client执行close,发送结束标记给server
  9. server接收到结束标记,执行close,结束交互 socket交互

客户端和服务端

服务端示例代码如下:

/home/unpv13e-master/intro/daytimetcpsrv.c

#include        "unp.h"
#include        <time.h>

int
main(int argc, char **argv)
{
        int                                     listenfd, connfd;
        struct sockaddr_in      servaddr;
        char                            buff[MAXLINE];
        time_t                          ticks;

        listenfd = Socket(AF_INET, SOCK_STREAM, 0);

        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family      = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port        = htons(13);   /* daytime server */

        Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

        Listen(listenfd, LISTENQ);

        for ( ; ; ) {
                connfd = Accept(listenfd, (SA *) NULL, NULL);

        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buff, strlen(buff));

                Close(connfd);
        }
}

客户端代码示例如下:

/home/unpv13e-master/intro/daytimetcpcli.c(本示例改动了部分代码)

#include <unp.h>
#include "apue.h"

int main(int argc, char **argv) 
{
    int sockfd,n;
    char recvline[MAXLINE+1];
    struct sockaddr_in servaddr;

    if (argc != 2)
    {
        err_quit("usage: a.out <IPaddress>");
    }
    
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0))<0)
    {
        err_sys("socket error");
    }
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(13);
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        err_quit("inet_pton error for %s", argv[1]);
    }
    if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
    {
        err_sys("connect error");
    }
    while ((n = read(sockfd, recvline, MAXLINE)) > 0)
    {
        recvline[n] = 0;
        if (fputs(recvline, stdout) == EOF)
        {
            err_sys("fputs error");
        }
    }
    if (n < 0)
    {
        err_sys("read error");
    }
    exit(0);
}

运行结果如下:

#服务端启动
sudo ./daytimetcpsrv
#客户端启动
sudo ./daytimetcpcli 127.0.0.1
#运行结果
Fri Jul 22 14:10:35 2022

socket

对于client/server,它们启动的第一步都是执行socket方法。

/* Create a new socket of type TYPE in domain DOMAIN, using
   protocol PROTOCOL.  If PROTOCOL is zero, one is chosen automatically.
   Returns a file descriptor for the new socket, or -1 for errors.  */
extern int socket (int __domain, int __type, int __protocol) __THROW;

参数说明

__domain

也就是protocol family,协议簇

__domain描述
AF_INETIPv4协议
AF_INET6IPv6协议
AF_LOCALunix协议
AF_ROUTE路由socket
AF_KEYkey socket

__type

待创建的socket类型

__type描述
SOCK_STREAMstream socket
SOCK_DGRAMdatagram socket
SOKC_SEQPACKETsequenced packet socket
SOCK_RAWraw socket

__protocol

socket协议类型

__protocol描述
IPPROTO_TCPTCP协议
IPPROTO_UDPUDP协议
IPPROTO_SCTPSCTP协议

三者组合

支持AF_INETAF_INET6AF_LOCALAF_ROUTEAF_KEY
SOCK_STREAMTCP|SCTPTCP|SCTPYes
SOCK_DGRAMUDPUDPYes
SOKC_SEQPACKETSCTPSCTPYes
SOCK_RAWIPv4IPv6YesYes

返回值

返回值说明
非负整数fd(socket对应的文件描述符)
-1执行失败

作用

为了执行网络IO,进程第一步要做的事就是调用socket方法,指定协议项(tcp/udp/sctp,ipv4/ipv6 ...)

bind

struct sockaddr
{
usigned short int sa_family;	/* Common data: address family and length.  */
char sa_data[14];		/* Address data.  */
};

/* Give the socket FD the local address ADDR (which is LEN bytes long).  */
extern int bind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen) __THROW;

参数说明

sockfd:为socket()方法返回的fd myaddr:socket address的指针(IP和端口信息) addrlen:struct sockaddr的长度

返回值

返回值说明
0执行成功
-1执行失败

作用

给socket分配一个协议地址,socket与端口绑定

端口如果为0,则由kernel来决定端口号

ip如果为0,则由kernel来决定ip

对于由kernel决定的信息,可以通过getsockname方法来获取。

对于服务端来说,我们需要指定一个端口号,客户端需要知道ip和port才能访问该服务。服务端是对外提供服务的,访问地址和端口信息是公开的。

但也有例外,比如注册发现机制的服务端来说,是不需要指定端口。

对于客户端来说,是无需执行bind操作的。

注:对于服务端,在绑定时,如果未指定一个ip地址,它会在接收到客户端发送SYN信息时,将SYN信息中的ip地址作为服务端的ip地址。

connect

/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
   For connectionless socket types, just set the default address to send to
   and the only address from which to accept transmissions.
   Return 0 on success, -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int connect (int sockfd, const struct sockaddr *servaddr, socklen_t __len);

参数说明

参数同bind

但这里的第二个参数,是远程的ip和端口信息

返回值

返回值说明
0执行成功
-1执行失败

作用

该方法会在连接建立或异常时,才会返回

对于tcp连接来说,过程包括:三次握手

  1. client发送SYN后,未收到服务端的<SYN, ACK>, ETIMEDOUT
  2. 如果服务端收到SYN后,响应一个RST,表示服务端没有处理该连接,客户端此时会有ECONNREFUSED错误返回
    • 请求的端口无对应服务
    • tcp剔除对应连接
    • tcp收到一个不存在的连接片段信息
  3. client收到ICMP不可达信息,它会继续尝试发送SYN,如果在固定的超时时间内,没有返回信息,它会返回EHOSTUNREACH或者ENETUNREACH

listen

/* Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.  */
extern int listen (int sockfd, int backlog) __THROW;

参数说明

  • sockfd: socket-fd,socket描述符

kernel会为任何一个给定的监听socket维护两个队列(如下图):

  1. 未完成连接队列
  2. 已完成连接队列

socket-listen

返回值

返回值说明
0执行成功
-1执行失败

作用

socket创建的套接字,默认是一个主动套接字,也就是我们所说的客户端套接字,将会调用connect方法的套接字。

listen可以将主动套接字转换成被动套接字,指示kernel应接受指示该套接字的连接请求。tcp状态由CLOSED转换成LISTEN

backlog的作用:当队列长度超过该设置值时,tcp连接请求将会被忽略。但tcp会重试发送请求,很可能后面就会接受该tcp请求。

accept

/* Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int accept (int __fd, const struct sockaddr cliaddr,
		   socklen_t *__restrict addrlen);

参数说明

  • cliaddr: 已连接的客户端地址信息
  • addrlen: cliaddr长度

返回值

返回值说明
非负整数已建立连接的描述符
-1执行失败

作用

返回下一个已完成的连接(从连接队列队首获取),如果连接队列为空,该方法会阻塞等待。

返回值是一个已建立连接的描述符,该描述符是由kernel自动创建。失败会返回异常值

close

#include <unistd.h>
/* Close the file descriptor FD.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int close (int sockfd);

参数说明

  • sockfd: 待关闭的socket的描述符

返回值

返回值说明
0执行成功
-1执行失败

作用

标记socket为closed状态,对应的描述符不能再被进程使用,即:不能被read/write使用。但如果缓冲中还有数据,会被发送到对应端。