unix环境网络编程笔记

struct sockaddr
位于头文件:<sys/socket.h>
通用套接字地质结构,代替void*的作用。

struct sockaddr_in
位于头文件:<netinet/in.h>
网际套接字地址结构,包括地址族,ip地址和端口号,还有补充字节。

function:
void bzero(void* src, size_t n)
void bcopy(const void src, void dest, size_t nbytes)
位于头文件:<cstring>
bzero将以src开始,长度为n字节的内存set-zero。
bcopy将指定数目的字节从src拷贝到dest。

function: htons, htonl, ntohs, ntohl
位于头文件:<netinet/in.h>
h代表host,n代表network,s代表uint16_t,l代表uint32_t,这四个函数用来在主机字节序和网络字节序转换2字节或者4字节数据。

function:inet_aton inet_ntoa inet_addr
位于头文件:<arpa/inet.h>
int inet_aton(const char strptr, struct in_addr addrptr)
从点分十进制串转换为二进制值,若字符串有效则返回1,否则返回0。
in_addr_t inet_addr(const char* strptr)
若字符串有效则返回二进制值,否则返回INADDR_NONE(废弃)
char* inet_ntoa(struct in_addr inaddr)
从二进制值转换为点分十进制串,不可重入,若inaddr不合法则返回nullptr。

function: inet_pton inet_ntop
在ubuntu上位于头文件:<arpa/inet.h>
int inet_pton(int family, const char strptr, void addrptr)
const char inet_ntop(int family, const void addrptr, char* strptr, size_t len)
inet_pton是一个ip地址转换函数,将点分十进制字符串转为二进制整数,如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。
对应的inet_ntop将二进制整数转为点分十进制字符串,如果成功返回存储区指针,否则返回nullptr。

function: socket, connect, bind, accept, close
位于头文件:<sys/socket.h>
套接字函数,具体用法见后。
bind函数将本地协议地址赋予一个套接字,如无错误发生,则bind()返回0。否则的话,将返回-1。
accept函数在一个监听套接字上接收一个连接。若失败返回-1,否则返回新的套接字描述符。第二个和第三个参数可以被置为nullptr指针,表示不关心连接套接字的具体信息。本函数从监听套接字的等待连接队列中抽取第一个连接,addr参数为一个返回参数,其中填写的是为通讯层所知的连接实体地址。addr参数的实际格式由通讯时产生的地址族确定。addrlen参数也是一个返回参数,在调用时初始化为addr所指的地址空间;在调用结束时它包含了实际返回的地址的长度(用字节数表示)。该函数与SOCK_STREAM类型的面向连接的套接口一起使用。如果addr与addrlen中有一个为零NULL,将不返回所接受的套接口远程地址的任何信息。
close函数关闭一个套接字,并关闭tcp连接,若成功返回0,失败返回-1。

function: read, write
位于头文件:<unistd.h>
read函数从文件描述符中读取数据存储缓存中,如果遇到EOF返回0,遇到错误返回-1,否则返回读到的字节数。
write函数将缓冲区中的数据写入文件,如果遇到错误则返回-1,否则返回实际写入的字节数。写入过程可能被信号中断,故返回值不一定等于要写入的数据。

function: puts
位于头文件:<cstdio>
将字符串输出到标准输出,将'0'换为回车换行。

三次握手:
一般服务器端通过socket,bind和listen创建一个监听套接字,该套接字处于LISTEN状态。
1.客户端调用connect函数,发送SYN分节,带有客户端初始序列号。
2.服务器端回复一个SYN+ACK分节,确认客户端初始序列号并提供服务器端初始序列号,并在监听套接字的半连接队列里记录相应信息。
3.客户端发送ACK分节,套接字变为ESTABLISHED状态,connect函数从阻塞中返回。
4.服务器端接收ACK分节,将半连接队列里对应的项放入连接队列,当有accept函数阻塞在该监听套接字上时,为连接队列里第一项分配一个套接字描述符并返回,该套接字处于ESTABLISHED状态。

主动执行关闭的那一端最后会进入TIME_WAIT状态,持续时间为2MSL,两个理由:
1.可靠的完成tcp关闭,确保最后一个ack丢失的时候可以重传。
2.确保所有属于本连接的tcp分节都在网络中消逝。

端口号被划分为三段:
1.众所周知端口0~1023.在unix系统中这些端口号只能赋予特权用户进程的套接字。
2.已登记端口1024~49151.
3.动态或私有端口49152~65535.

缓冲区大小限制
ipv4字节报最大字节数包括首部一共是65535字节,要求最小链路MTU字节为68字节,必须支持的最小数据报大小为576字节。tcp协议的MSS用于向对端通知每个分节中能发送的最大字节数,目的是避免分片,一般设置为路径MTU-IP首部长度-TCP首部长度1460。

TCP输出
每个套接字都有一个发送缓冲区,可以使用套接字选项改变该缓冲区大小。对于阻塞的套接字的write调用,就是将数据都写入此缓冲区,如果当前发送缓冲区不足以装下write的字节,则阻塞并等待发送缓冲区空出足够的位置。本端tcp将发送缓冲区的数据以MSS或者更小的块作为一个TCP分节,安上tcp首部并交给ip层程序。(如果对端没有告诉MSS,则设置为536,因为ip最小数据报大小为576,减去两个首部40字节,最少支持536字节的数据块)。ip层程序将每个数据块安上ip首部,通过查找路由表以确定下一跳地址,交给对应的链路。如果当前链路的缓存已满,会将其丢弃并返回给tcp层,由其在稍后进行重新发送。

UDP输出
UDP套接字发送缓冲区的大小仅仅是该UDP数据报大小上限,如果应用程序写一个大于该上限的字节数,则会返回EMSGSIZE错误。(因为UDP不需要ack分节确保数据可靠性,因此不需要缓存发出的数据报)UDP数据报更容易在ip层产生分片,因为其不像TCP协议一样通过MSS避免分片。

字节流套接字上的read,write函数所展现的行为不同于通常的文件io,
其返回后读到或者写入的数据都可能小于设定的大小。提供readn,writen和readline等函数读取或写入设定的字节。

基本tcp套接字编程函数
位于头文件:<sys/socket.h>

int socket(int family, int type, int protocal);
若成功则返回描述符,失败返回-1。
family可取以下:AF_INET AF_INET6 AF_ROUTE AF_LOCAL AF_KEY.
type可取:SOCK_STREAM SOCK_DGRAM SOCK_RAW等。

int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
若成功则返回0,失败返回-1。见三次握手。
出错的情况:
1.没有收到SYN分节的响应,重传多次后仍没有收到,返回ETIMEOUT错误。
2.对SYN分节的相应是RST分节,表示服务器没有在指定的端口上没有等待进程,返回ECONNREFUSED错误。
注:产生RST的三个条件:目的为某端口的SYN分节到达,但该端口上没有等待进程;想要结束某个TCP连接;接收到一个不存在的连接上的分节。
3.发出的SYN分节在某个路由器上收到目的不可达的ICMP报文,内核处理方法如情况1,重传,如果仍没有收到,返回EHOSTUNREACH或者ENETUNREACH(过时)错误。
connect调用失败的套接字不能再次调用connect,只能关闭。
在调用connect函数之前不必非得调用bind函数为套接字指示符绑定ip地址和端口号,而是由内核确定源ip地址和选择一个临时端口作为端口号。

int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);
若成功返回0,出错返回-1。返回错误常见EADDRINUSE,地址已使用。
把一个协议地址赋予套接字。可以只绑定一个ip地址,也可以只绑定一个端口号,可以两者都绑定也可以都不绑定。在connect或者listen调用之前可以不调用bind,这时由内核分配ip地址和端口号,对于客户端来说很平常,但服务器很少使用临时端口,毕竟需要客户端通过该端口号连接。(RPC服务器例外)
如果服务器没有把ip地址绑定到它的套接字上,内核就把接收的SYN分节的目的ip当作该套接字的源ip地址。

int listen(int sockfd, int backlog);
若成功返回0,出错返回-1。
该函数将默认的主动连接的套接字转换为被动连接,指示内核应该接受指向该套接字的连接请求,第二个参数指定了相应套接字队列的最大个数。
注:内核给每一个被动连接的套接字维护两个队列(SYN_RCVD):未完成连接队列和已完成连接队列(ESTABLISHED)
backlog曾经被规定为这两个队列之和的最大值。源自Berkeley的实现为其设置为模糊因子,表示未处理队列最大长度。
在三路握手正确连接的前提下,未完成连接队列中每一项的最大存留时间为一个RTT。
当一个客户SYN分节到达,且未完成连接队列已满时,不发送RST分节而是忽略该SYN分节,希望重传机制处理这个问题。
在三路握手完成之后,accept函数调用之前,到达的数据由服务器存储,最大数据为相应的已连接套接字的接收缓冲区大小。
syn泛滥攻击,第二个参数应该修改为已连接队列的最大值。

int accept(int sockdf, struct sockaddr* cliaddr, socklen_t* addrlen);
若成功则返回描述符,失败返回-1。
从监听套接字的已连接队列中取出第一项并构建已连接套接字描述符,返回。如果已连接队列是空的则阻塞线程。

int close(int sockfd);
位于头文件:<unistd.h>
若成功返回0,出错返回-1。
默认行为:将该套接字标记成已关闭,然后立即返回调用进程。该套接字描述符不能再由调用进程使用,Tcp丢弃接收缓冲区中的内容,并尝试将发送缓冲区的数据发送给接收端并完成Tcp正常关闭。此行为相当于将套接字交由内核管理。
SO_LINGER套接字选项可以用来更改默认行为。

  • l_onoff值为0:相当于默认行为。
  • l_onoff值非0,l_linger为0:将发送RST复位连接,所有发送、接收缓冲区的数据都被丢弃,socket资源被释放,不进入TIME_WAIT状态。接收端的read会得到ECONNRESET错误。
  • l_onoff值非0,l_linger非0,socket是阻塞的:则不会立即返回,而是等待l_linger长的时间,在l_linger时间内如果发送缓冲区里的数据发送并被B端确认,则返回,之后任然是“优雅”的关闭,A端进入TIME_WAIT状态;如果发送缓冲区的数据没有发送完毕或者没有收到B端确认,则返回,同时内核放弃没有发送的数据或是不再等待B端的确认,直接发送RST复位连接,A端不进入TIME_WAIT状态。
  • l_onoff值非0,l_linger非0,socket是非阻塞的:立即返回,如果从调用到返回的时间内,不能完成发送缓冲区数据的发送和对端的确认,则closesocket返回EWOULDBLOCK。

原文链接:https://blog.csdn.net/easyioc...
另见:unix网络编程7.5.6关于shutdown和close的总结,十分全面。
注:在陈硕大佬的网络编程实践和一些博客中记录,linux系统下的close不同:只要TCP栈的读缓冲里还有未读取(read)数据,则调用close时会直接向对端发送RST,而不是如同unix网络编程中记录的那样默认丢弃接收缓冲区中的数据,需要进一步测试。
见链接:https://blog.csdn.net/weixin_...

int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t* addrlen);
int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t* addrlen);
位于头文件:<sys/socket.h>
若成功返回0,失败返回-1.
获取sockfd绑定的本地/远端的ip地址和端口号。

pid_t fork(void)
位于头文件:<unistd.h>
返回值子进程中为0,父进程为子进程id,出错则返回-1。
fork被用来创建一个新进程,子进程获得父进程的数据空间,堆和栈的复制品。注意fork和io函数之间的关系,所有被父进程打开的描述符都被复制到子进程中,父子进程相同的描述符共享一个文件表项。这种共享方式使得父子进程对共享的文件使用同一个偏移量。这种情况下如果不对父子进程做同步处理,则对同一个文件的输出是混合在一起的。因此要么父进程等待子进程执行完毕,要么互不干扰对方使用的文件描述符。

函数vfork和fork的区别:vfork也创建一个子进程,但其不复制父进程的信息而是和其共享,vfork保证子进程先执行,并且只有在子进程调用exec或者exit函数之后父进程才可以继续运行。vfork就是为了exec而生,因为其避免了子进程拷贝父进程的各种信息--但是如今fork函数一般会采用写时复制的方法,因此fork+exec的开销会小很多。
注:在vfork中调用return会导致父进程一起挂掉,因为其共享父进程的信息,return会触发mian函数的局部变量析构并弹栈,而直接使用exit函数则不会发生这种情况。
关于fork和vfork的区别见链接:https://www.cnblogs.com/19322...

exec系列函数
exec系列函数提供了在进程中切换为另一个进程的方法,该系列函数一共有六个,在提供的接口上有一些不同,但最终是通过调用execve系统调用完成。对打开文件的处理与每个描述符的exec关闭标志值有关,进程中每个文件描述符有一个exec关闭标志(FD_CLOEXEC),若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开,利用这一点可以实现I/O重定向。
见链接:https://blog.csdn.net/amoscyk...

第一个daytime服务器:

err_sys.h/cpp
#include <cstdio>
void release_assert(bool error, const char* message);

void release_assert(bool error, const char* message) {
    if(error == true) {
        fprintf(stderr, message);
        std::abort();
    }
}
#include <netinet/in.h>
#include <ctime>
#include "err_sys.h"
#include <cstring>
#include <sys/socket.h>
#include <unistd.h>
#include <iostream>
#include <arpa/inet.h>

int main()
{
    struct sockaddr_in servaddr;
    char buf[1024];

    int 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(12345);

    int result = bind(listenfd, (sockaddr*)&servaddr, sizeof(servaddr));
    release_assert(result != 0, "bind error!");
    result = listen(listenfd, 1024);
    while(true) {
        sockaddr_in client_addr;
        socklen_t len;
        std::cout << "listening..." << std::endl;
        int connfd = accept(listenfd, (sockaddr*)&client_addr, &len);
        release_assert(connfd == -1, "accept error!");
        char buffer[64] = {0};
        std::cout << "accept successful : ";
        const char* p = inet_ntop(AF_INET, &client_addr.sin_addr, buffer, sizeof(buffer));
        release_assert(p == nullptr, "inet_ntop error!");
        std::cout << "ip address : "<<buffer << " "<< "port : " << ntohs(client_addr.sin_port)<<std::endl;
        time_t ticks = time(nullptr);
        snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
        result = write(connfd, buf, strlen(buf));
        release_assert(result != strlen(buf), "write error!");
        result = close(connfd);
        release_assert(result != 0, "close error!");
    }
    return 0;
}

客户端:

#include <netinet/in.h>
#include "err_sys.h"
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdio>

int main()
{
    int sockfd, n;
    char recvline[1025];
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    release_assert(sockfd < 0, "socket error!");
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(12345);
    int result = inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    release_assert(result <= 0, "inet_pton error!");

    result = connect(sockfd,(sockaddr*)&servaddr,sizeof(servaddr));
    release_assert(result < 0, "connect error!");

    while((n = read(sockfd, recvline, 1024)) > 0) {
        recvline[n] = '\0';
        puts(recvline);
    }
    release_assert(n < 0, "read error!");
    return 0;
}

基于多进程阻塞套接字的echo服务。
服务端:

#include <iostream>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

typedef  void Sigfunc(int);
Sigfunc* signaler(int signo, Sigfunc *func) {
  struct sigaction act, oact;
  act.sa_handler = func;
  sigemptyset(&act.sa_mask);
  act.sa_flags = 0;
  if(sigaction(signo, &act, &oact) < 0) {
    return SIG_ERR;
  }
  return oact.sa_handler;
}

void sig_chld(int signo) {
  pid_t pid;
  int stat;
  while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
    printf("child %d terminated\n", pid);
  return;
}

void echo(int clientfd) {
  std::cout << "connect successful!\n";
  struct sockaddr_in client_addr;
  socklen_t len = sizeof(client_addr);
  getpeername(clientfd, (sockaddr*)&client_addr, &len);
  char buffer[64] = {0};
  const char* ptr = inet_ntop(AF_INET, &client_addr.sin_addr, buffer, sizeof(buffer));
  assert(ptr != nullptr);
  std::cout << "client addr : " << ptr << ", port : " << ntohs(client_addr.sin_port) << "\n";
  std::string recv_content;
  int n = 0;
  char buf[1025];
  while((n = read(clientfd, buf, 1024)) > 0) {
    buf[n] = '\0';
    recv_content.append(buf);
  }
  assert(n == 0);
  n = write(clientfd, recv_content.c_str(), recv_content.size());
  assert(n == static_cast<int>(recv_content.size()));
  close(clientfd);
  std::cout << "cliend close. \n";
}

int main()
{
  int listenfd = socket(AF_INET, SOCK_STREAM, 0);
  assert(listenfd > 0);

  struct sockaddr_in addr_;
  bzero(&addr_, sizeof(addr_));
  addr_.sin_family = AF_INET;
  int result = inet_pton(AF_INET, "127.0.0.1", &addr_.sin_addr);
  assert(result > 0);
  addr_.sin_port = htons(12340);

  result = bind(listenfd, (struct sockaddr*)&addr_, sizeof(addr_));
  assert(result == 0);
  result = listen(listenfd, 1024);
  assert(result == 0);
  signaler(SIGCHLD, sig_chld);
  while(true) {
    std::cout << "parent process listening... \n";
    sockaddr_in clientaddr;
    socklen_t clientlen = sizeof(clientaddr);
    int clientfd;
    while(true) {
      clientfd = accept(listenfd, (sockaddr*)&clientaddr, &clientlen);
      if(clientfd == -1) {
        assert(errno == EINTR);
        continue;
      }
      else {
        break;
      }
    }
    std::cout << "accept successful ! \n";
    pid_t child_pid = fork();
    if(child_pid == 0) {
      close(listenfd);
      echo(clientfd);
      exit(0);
    }
    close(clientfd);
  }
  return 0;
}

客户端:

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <cassert>
#include <unistd.h>

int main() {
  for(int i = 0; i < 5; ++i) {
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in server_addr;
  bzero(&server_addr, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  int result = inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
  assert(result > 0);
  server_addr.sin_port = htons(12340);

  result = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
  assert(result == 0);
  std::cout << "connect successful !\n";
  const char* ptr = "hello world!";
  int n = write(sockfd, ptr, strlen(ptr));
  assert(n == strlen(ptr));
  result = shutdown(sockfd, SHUT_WR);
  assert(result == 0);
  char buf[65];
  std::string recv_content;
  while((n = read(sockfd, buf, sizeof(buf) - 1)) > 0) {
    buf[n] = '\0';
    recv_content.append(buf);
  }
  assert(n == 0);
  std::cout << "recv : " << recv_content << std::endl;
  result = close(sockfd);
  assert(result == 0);
  std::cout << "connect end. \n";
  }
  return 0;
}

上述代码通过捕获信号处理子进程,具体见unix环境编程。
这里说明wait和waitpid的区别,为什么在处理僵死进程的时候使用waitpid而不是wait。首先,unix信号在信号处理函数执行期间可以被阻塞,等信号处理函数结束之后再调用对应的信号处理函数,但是信号是不排队的,即如果很短时间内接收数个SIGCHLD信号,也只会阻塞一个。这种情况下使用wait函数,首先wait函数是阻塞的,所以不能while循环使用,因为我们不希望在信号处理函数中阻塞等待,只是想处理掉当前的僵死进程。如果每个SIGCHLD信号处理函数只调用一次wait函数,由于信号不排队的性质,信号处理函数的调用次数可能小于僵死进程的个数。
所以应该使用waitpid的非阻塞模式,这样可以在每次SIGCHLD信号处理函数中循环回收僵死进程,直到剩余的子进程都是正在运行或者已经没有子进程在运行,然后返回即可。这种模式下SIGCHLD信号的产生代表有一个或多个僵死进程产生,而不具有了计数功能(信号不排队,本来就不能用作计数功能)。信号的这种阻塞+不排队性质 与 循环+非阻塞 的处理方法天然兼容,细细体会。

理清服务端/客户端在发生各种正确终止或错误终止时tcp协议如何断开连接是非常重要的。
1.进程提前终止
进程终止前关闭所有描述符,套接字描述符向对端发送FIN分节,对端回复FIN_ACK分节,对端并不知道本端进程已经推出,继续发送数据,本端回以RST分节,如果对端在接收到RST分节后依然向本端发送数据(实际上这无法避免,TCP是个字节流协议,一次write调用仅是将数据写入发送缓冲区,无法保证只发送一个TCP包),则内核向进程发送SIGPIPE信号,默认行为是终止进程。
这是应该避免的行为,试想如果服务器端在向客户端发送数据时客户端进程提前终止,那么服务器进程将会被终止。解决办法就是将该信号处理设置为忽略,这样向一个接受了RST包的套接字写数据则会返回EPIPE错误,我们在进程中处理该套接字即可。
2.主机崩溃
主机崩溃和进程终止的区别是内核是否介入进行清理工作,在网络编程中关注的清理工作就是close套接字描述符,进而发送FIN分节或者RST分节。进程崩溃后不会有任何数据接收或者返回,这种情况一般要靠TCP协议的超时-重传-返回timeout错误处理。注意,如果进程崩溃时两端没有数据交互,则存活的一端无法主动发现另一端已经崩溃,因为TCP状态中的ESTABLISHED状态没有超时机制。这也就是套接字选项SO_KEEPALIVE的作用(以及应用层心跳包的作用)。
3.主机崩溃后重启
对意外的TCP包都是通过返回RST分节进行处理。
4.服务器主机关机
内核仍然介入处理,同1。

阅读 424

推荐阅读