基础TCP套接字编程

基本TCP客户服务器模型

基础TCP套接字编程_第1张图片
基本TCP客户服务器模型.jpg

IPV4套接字结构

  • struct sockaddr

    /* 通用套接字地址 */
    #include 
    
    struct sockaddr 
    {
        uint8_t       sa_len;          /* 1字节 */
        sa_family_t   sa_family;       /* 1字节 */
        char          s_data[14];      /* 14字节 包含目标地址和端口信息 */
    };
    
  • struct sockaddr_in

    /* IPV4地址,网络字节序 */
    typedef uint32_t in_addr_t;
    struct in_addr 
    {
        in_addr_t s_addr;    /* address in network byte order */
    };
    
    /* IPV4套接字地址 */
    #include 
    
    struct sockaddr_in 
    {
        sa_family_t    sin_family;         /* address family: AF_INET   */
        uint16_t       sin_port;           /* port in network byte order  2字节 */
        struct         in_addr sin_addr;   /* internet address  4 字节 */
        char           sin_zero[8];
    };
    
struct sockaddr 和 struct sockaddr_in 区别和联系
  • 2者都是16字节长度
  • connect 等后面系统调用使用的地址是 sockaddr

socket 函数

#include 

int socket(int family, int type, int protocol)  成功返回非负描述符,若出错返回-1

family指明协议族:PF_INETPF_INET6PF_LOCAL....
type指明套接字类型:SOCK_STREAM(TCP),SOCK_DGRAM(UDP),SOCK_RAW(原始套接字)
protocol指明某个协议类型的常值,现在基本废弃,一般设为0。

socket成功返回套接字描述符。

  • 示例:创建一个非阻塞的ipv4套接字
    int createSocket()
    {
        int ret = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
        if (ret == -1) {
            SYSFATAL("socket() Error");
        }
        return ret;
    }
    

connect 函数

#include 

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
成功则为0, 若出错则为-1

connect用来与远程服务器建立连接。若是TCP套接字,调用connect将激发3路握手的过程,仅在连接建立成功或者出错时才返回

  • 若TCP客户没有收到SYN分节的响应,则返回 ETIMEDOUT 错误。
  • 若对客户的SYN的响应式RST,则表明服务器主机在我们指定的端口上没有进程在等待与之连接, 这是一种"硬错误",会立即返回 ECONNREFUSED 错误。

产生RST的3个条件:

  • 目的地为某端口SYN到达,而该端口没有上没有监听的服务、
  • TCP想取消一个已有连接
  • TCP接受到一个根本不存在的连接
基础TCP套接字编程_第2张图片
Paste_Image.png

connect函数会导致当前套接字状态从 CLOSED 状态(该套接字自从由socket函数创建以来一直所处的状态)
转移到SYN_SENT状态,若成功再转移到 ESTABLISHED 状态。每次connect失败后,都必须close当前的套接字描述符并重新调用 socket。

关于阻塞和非阻塞的connect

在 socket 是阻塞模式下 connect 函数会一直到有明确的结果才会返回(或连接成功或连接失败),
在实际项目中,我们一般倾向使用所谓的异步的 connect 技术,或者叫非阻塞的 connect。

bind 函数

#include 

int bind(int sockfd, const struct sockaddr *myaddr, socklen_ addrlen);    
若成功则为0,若出错则为-1

bind函数把一个本地协议地址赋予一个套接字。

struct sockaddr_in addr_;
addr_.sin_family = AF_INET;
in_addr_t ip = INADDR_ANY // INADDR_LOOPBACK 
addr_.sin_port = htons(port);
addr_.sin_addr.s_addr = htonl(ip);

INADDR_ANY 表示地址是 0.0.0.0 (主机序的数值表达形式)
INADDR_LOOPBACK 表示 127.0.0.1 (主机序的数值表达形式)

假设一台机器对外访问的ip地址是120.55.94.78,这台机器在当前局域网的地址是192.168.1.104;同时这台机器有本地回环地址127.0.0.1。

如果你指向本机上可以访问,那么你 bind 函数中的地址就可以使用127.0.0.1 或 INADDR_LOOPBACK
如果你的服务只想被局域网内部机器访问,bind 函数的地址可以使用192.168.1.104。
如果 希望这个服务可以被公网访问,你就可以使用地址 0.0.0.0 或 INADDR_ANY

  • 服务器在启动时捆绑它总所周知的端口,如果一个TCP客户或者服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。

  • 进程也可以把一个特定的IP地址捆绑到它的套接字上。

    • 对于TCP客户,这就为该套接字上发送的IP数据报指派了源IP地址,通常,TCP客户不用设置这一步,因为内核会根据外出网络接口来选择源IP地址。
    • 对于TCP服务器,这就限定了该套接字只接受那些目的地为这个IP地址的客户连接,如果TCP服务器没有把IP地址捆绑到它的套接字上,内核就把客户发送SYN的目的IP地址作为服务器的IP地址。

listen函数

#include 

int listen(int sockfd, int backlog);  若成功则为0,若出错则为-1
  • 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说将调用connect发起连接的客户套接字。而listen函数把一个未连接的套接字转换为一个被动套接字。
  • 理解backlog参数
基础TCP套接字编程_第3张图片
TCP为监听套接字维护2个队列.jpg

内核为任何一个给定的监听套接字维护2个队列。

  • 未完成的连接队列,每一个SYN分节对应其中一项
  • 已完成的连接队列,每个已完成TCP三路握手过程的客户。

当来自客户的SYN到达时,TCP在 未完成连接队列 中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,捎带对客户SYN的ACK。
这一项一直保留在未完成连接队列中,直到3路握手的第三个分节(客户对服务器的SYN的ACK)到达或者该项超时。如果三路握手正常完成,该项就从未完成连接队列移到已完成的连接队列队尾。这里的backlog,在 Linux 中表示已完成 (ESTABLISHED) 且未 accept 的队列大小。

accept 函数

#include 

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
成功返回非负描述符, 若出错返回-1

accept函数由TCP服务器调用,用于从已完成连接队列头部返回一个已经完成连接的客户,如果已完成连接队列为空,则该进程会被投入睡眠。成功,返回值是一个由内核产生的全新描述符,代表与客户的连接,称之为已连接套接字。

close函数

#include 

int close(int sockfd);      返回:若成功则为0,如出错则为-1

close这个函数会对套接字引用计数减1,一旦发现套接字引用计数到 0,就会对套接字进行彻底释放,并且会关闭 TCP 两个方向的数据流。

在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。

shutdown函数

#include 

int shutdown(int sockfd, int howto);   返回值:成功为0,出错则为-1

函数行为依赖于howto参数的值。

  • SHUT_RD:关闭连接的读的一半
    套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。
  • SHUT_WR:关闭连接的写这一半--这称为半关闭
    当前留在套接字发送缓冲区的数据将被发送掉,然后TCP正常连接终止序列。
  • SHUT_RDWR:连接的读半部和写半部都关闭。

shutdown函数和close函数相比,close有2个限制:

  • close函数把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。shutdown函数不管引用计数都会激发TCP的正常连接终止序列。
  • close终止读和写2个方向的数据传送。

inet_pton和inet_ntop函数

#include  

int inet_pton(int family, const char* strptr, void *addrptr); 
    返回值:若成功则为1,若输入不是有效的表达格式则为0,若出错则为1,且errno置为EAFNOSUPPORT
const char* inet_ntop(int family, const void* addrptr, char *strptr, size_t len);
    返回值:若成功则为指向结果的指针,若出错则为NULL,且errno置为EAFNOSUPPORT。

eg:  inet_pton(AF_INET, ip_addr, &(cliname.sin_addr));
eg:  inet_ntop(AF_INET, (&servaddr.sin_addr), buf, 64);

inet_pton负责将字符串转为数值格式。
inet_ntop负责将数值转为字符串格式。
p(presentation)和数值n(numeric)。

htons和htonl函数
#include 

uint16_t htons(uint16_t hostbit16value);
uint32_t htonl(uint32_t host32bitvalue);

均返回,网络字节序的值。

将主机序转为网络序(大端字节序)。

举例:时间服务器

客户

客户端通过创建 socket,connect 发起连接建立请求。

int main(int argc, char** argv)
{
    int sockfd, n;
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    
    if (argc != 2)
        err_quit("usage ");

    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(8080);
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
        err_sys("inet_pton error");

    if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) < 0)  
        err_sys("connect error");
    
    while( (n = read(sockfd, buf, MAXLINE)) > 0) {
        buf[MAXLINE] = '\0';
        if (fputs(buf, stdout) == EOF)
            err_sys("fpus error");
    }
    if (n < 0)
        err_sys("read error");
}
服务器

服务器端通过创建 socket,bind,listen 完成初始化,通过 accept 完成连接的建立。

int main(int argc, char**argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    time_t ticks;
    
    if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_sys("sockfd error");

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
    
    listen(listenfd, LISTENQ);
    
    while(1) {
        connfd = accept(listenfd, NULL, NULL);
        ticks = time(NULL);
        snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
        write(connfd, buf, strlen(buf));
        close(connfd);
    }
}

参考资料
1、《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff

你可能感兴趣的:(基础TCP套接字编程)