Linux| Socket网络编程

基于TCP网络编程API

什么是 socket?

socket起源于UNIX,而 UNIX/Linux 基本哲学之一就是”一切皆文件”,都可以用打开(open) -> 读写(write/read) -> 关闭(close)模式来操作。socket其实就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写、 打开、关闭)。

socket 的基本操作

socketopen-write/read-close模式的一种实现,那么socket就提供了这些操作对应的函数接口,本文将以TCP协议通信socket为例,讲解相关的函数接口

TCP三次握手
TCP三次握手

TCP交互流程
TCP交互流程

socket 函数

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述符,而socket()用于创建一个socket描述符(socket description),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它进行一些读写操作

1
int socket(int domain, int type, int protocol);

参数简述

  • domain:协议域,又称为协议族(family)。常用的协议族有:AF_INEFAF_INET6AF_LOCAL等。协议族决定了socket的地址类型,在通信种必须采用对应的地址,如AF_INEF决定了要用ipv4地址(322)位与端口号(16位)
  • type:指定socket类型。常用的socket类型有:SOCK_STREAMSOCK_DGRAM等等。其中, SOCK_STREAM表示提供面向连接的稳定数据传输,即 TCP 协议。SOCK_DGRAM表示使用不连续、不可靠的数据包连接
  • protocol:指定协议。常用的协议有:IPPROTO_TCP, IPPTOTO_UDP, IPPROTO _ SCTPIPPROTO_TIPC 等,它们分别对应 TCP 传输协议、 UDP 传输协议、 STCP 传输协议、 TIPC 传输协议

bind 函数

bind() 函数把一个地址族中的特定地址赋给 socket

1
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

参数简述

  • sockfd:即socket描述字,它是通过socket()函数创建来唯一标识一个socket的。bind()函数就是将给这个描述字绑定一个名字
  • addr:一个 const struct sockaddr*指针,指向要绑定给 sockfd的协议地址。 这个地 结构根据地址创建 socket时的地址协议族的不同而不同
  • addrlen:对应的是地址的长度

listen 和 connect 函数

如果作为一个服务器,在调用socket()bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

1
int listen(int sockfd, int backlog);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的 socket 默认是一个主动类型的, listen 函数将 socket 变为被动类型的,等待客户的连接请求。

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

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接

accept 函数

TCP 服务器端依次调用 socket()bind()listen()之后,就会监听指定的socket 地址了。TCP 客户端依次调用 socket()connect()之后就会向 TCP 服务器发送了一个连接请求。 TCP 服务器监听到这个请求之后,就会调用accept()取接收请求,这样连接就建立好了。 之后就可以开始网络 I/O 操作了,即类同于普通文件的读写 I/O 操作。

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

accept函数的第一个参数为服务器的 socket 描述字;第二个参数为指向 struct sockaddr* 的指针,用于返回客户端的协议地址;第三个参数为协议地址的长度。如果 accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的 TCP 连接

注意accept的第一个参数为服务器的 socket 描述字,是服务器开始调用 socket()函数生成的,称为监听socket描述字;而 accept函数返回的是己连接的 socket描述字。 一个服务器通常仅仅只创建一个监听 socket 描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户创建了一个已连接 socket 描述字,当服务器完成了对某个客户的服务,相应的已连接 socket描述字就被关闭

read 和 write 函数

至此服务器与客户已经建立好连接了,可以调用网络 I/O 进行读写操作了,即实现了网络中不同进程之间的通信。网络 I/O 操作有下面几组

  • read() / write()
  • recv() / send()
  • readv() / writev()
  • recvmsg() / sendmsg()
  • recvfrom() / sendto()

比较常用的是readwrite

1
ssize_t read(int fd, void *buf, size_t count);

read()函数是负责从fd中读取内容。当读取成功时,read()返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。如果错误为 EINTR 说明读是由中断引起的,如果是 ECONNREST 表示网络连接出了问题。

  • socket 描述字 fd
  • 缓冲区 buf
  • 缓冲区长度 count
1
ssize_t write(int fd, void *buf, size_t count);

write()函数将buf中的 nbytes 字节内容写入文件描述符fd成功时返回写的字节数。失败时返回-1,并设置 errno 变量。在网络程序中,当我们向套接字文件描述符写时有两种可能: 1.write 的返回值大于0,表示写了部分或者是全部的数据; 2.返回的值小于 0,此时出现了错误。实际中要根据错误类型来处理。如果错误为 EINTR 表示在写的时候出现了中断错误。如果为 EPIPE 表示网络连接出现了问题(对方已经关闭了连接)

  • fd 表示 socket 描述字
  • buf表示缓冲区
  • count 表示缓冲区长度

close 函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的 socket描述字

1
2
3
#include <unistd.h>

int close(int fd);

close一个 TCP socket 的默认行为时,会把该 socket 标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为 readwrite 的第一个参数

实现简单的TCP-Server

该实现主要是了解一些相关API的调用,起主要参考的TCP交互流程即可 - 项目 - TCP-Server
TCP交互流程

网络字节序与主机序

  • 主机序 - 不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,称为主机序。最常见的就是大端和小端
    • Little Endian - 小端: 把地址低位存储值的低位,地址高位存储值的高位
    • Big Endian - 大端: 把地址低位存储值的高位,地址高位存储值的低位
  • 网络字节序 - 4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序
  • 网络协议也都是采用 Big Endian 的方式来传输数据的。 所以有时也会把 Big Endian 方式称之为网络字节序。 当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成网络字节序后再进行传输

参考