unix网络编程笔记

Posted sunny_ss12

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unix网络编程笔记相关的知识,希望对你有一定的参考价值。

第四章笔记

1. 基本Tcp客户端/服务器程序的套接字函数

基本Tcp客户端/服务器程序的套接字函数

2. socket函数:

int socket(int family,int type,int protocol);

(1)socket有三个函数,除了tcp udp外还支持许多协议。
(2)对于tcp协议:三个参数分别为AF_INET/AF_INET6、SOCK_STREAM、0
(3)对于udp协议:三个参数分别为AF_INET/AF_INET6、SOCK_DGRAM、0
(4)AF_LOCAL(或者是AF_UNIX)用于运行在相同机器的两个进程之间进行通信
(5)其他协议和参数含义,先pass
(6)返回值:如果失败返回-1,如果成功返回新创建的套接字描述符。每个进程在自己的进程空间里都有一个套接字描述表,该表中存放着套接字描述符和套接字数据结构地址的对应关系,而套接字数据结构都存放在操作系统的内核缓冲里。

3. connect函数:

 int connect(int sockfd,connect struct sockaddr* servaddr,socklen_t addrlen);

(1)正如第三章所示:第二个参数是通用的套接字地址结构指针,但是传入参数时需要指定具体的套接字地址结构
(2)客户在调用connect前不必非得调用bind函数,因为内核对确定源IP地址,并选择一个临时端口作为源端口
(3)connect激发Tcp三次握手过程。当客户收到三次握手的第二个分节时,connect就会返回,而服务器要直到收到三次握手的第三个分节才返回。
(4)connect可能出错的情况:
a: 若客户机与服务器断连,Tcp客户端没有收到SYN分节的响应,返回ETIMEDOUT错误。举例:对于4.4BSD,当内核发送SYN,若无响应6s后再发送,若仍无响应再24s后再发送,若总共等了75s仍未响应返回ETIMEDOUT
经过测试,运行time intro/daytimetcpcli 10.0.0.1,经过2m7s左右才返回connect time out的错误
b: 若服务器主机在我们指定的端口上没有进程在等待与之连接,则对客户的SYN的响应是RST。这是一种硬错误(hard error),当客户一接收到RST就马上返回ECONNREFUSED错误。
c: 当客户发出的SYN在中间的某个路由器引发了一个目的地不可达的ICMP错误,这认为是一种软错误,之后按第一种情况所属的时间间隔继续发SYN。若在某个规定时间仍未收到响应,则返回EHOSTUNREACH或ENETUNREACH错误。
d: 由于信号导致的调用中断错误EINTR。注意:即使connect这个函数返回EINT,我们也不能再次调用它,否则会立即返回一个错误(5.9)。所以出现EINTR可以采取和其他错误同样的处理方式,比如输出错误退出进程。
(4)当connect成功返回,当前套接字进入ESTABLISHED状态,即三次握手成功状态。若失败则该套接字不再使用,如果需要再次调用connect时,必须close当前的套接字都重新调用socket

4. bind函数:

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

(0)对于第二个参数const struct sockaddr* myaddr,指向要绑定给sockfd的协议地址。这个地址根据地址创建socket时的地址协议组的不同而不同,对于Ipv4要传递类型为sockaddr_in的地址,对于IPv6要传递类型为sockaddr_in6的地址,对于AF_UNIX要传递类型为sockaddr_un的地址。
(1)绑定的IP地址和端口号,可以指定通配IP地址和端口号0。如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UPD)时才选择一个本地IP地址。
(2)对于IPv4,通配地址由常量INADDR_ANY(0.0.0.0)来指定。但是IPv6的IP地址是128位,不是简单类型,不能像IPv4那样用简单数值常量表示。
(3)bind通配地址是在告知系统,如果系统是多宿主机(多个IP),我们将接受目的地址为任何本地接口的连接
IP地址用通配地址赋值代码:

struct sockaddr_in saddr;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY定义在头文件<netinet/in.h>
struct sockaddr_in6 saddr6;
saddr6.sin6_addr = in6addr_any;//系统预先分配in6addr_any变量并将其初始化常量IN6ADDR_ANY_INIT。头文件<netinet/in.h>含有in6addr_any的extern声明。

(3)传入bind的套接字地址IP和端口号不要忘记转换成网络字节序
(4)如果bind的是一个临时端口号,由于bind并不返回所选择的值,那么我们无法知道究竟bind了哪个端口号,可以调用函数getsockname来返回协议地址。
(5)bind返回的常见错误EADDRINUSE(地址已使用)
当绑定内置端口号(1-1024),必须有root权限,否则bind返回Permission denied错误。

5. listen:

int listen(int sockfd,int backlog)

(1)backlog的含义:内核为监听套接字维护的已完成连接队列(以完成三次握手,状态为ESTABLISHED,并正等待accpet)总个数的最大值
(2)backlog到底设置多少是合理的呢????
(3)当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,而不是立即响应RST。因为这种情况是暂时的,客户端会等一段时间会重发SYN,期望不久能在这些队列中找到可用空间。

6. accept:

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

(1)作用:用于从已完成连接队列返回下一个已完成连接。如果已完成连接队列为空,那么进程被进入睡眠(如果套接字为默认的阻塞方式)。
(2)如果对返回的套接字地址不感兴趣,cliaddr和addrlen可以设置为NULL
(3)accept返回的cliaddr,IP地址和端口号都是网络字节序,如果要打印出来查看,需要先转换成主机字节序,之后端口号可以直接打印,IP地址之后需要再次调用inet_ntop来获取字符串格式
(4)调用accpet时,如果出现EINTR或ECONNABORTED错误时可以忽略继续调用下一次accept。EINTR错误是当信号发生时出现调用中断错误,而ECONNABORTED错误是当客户端调用connect与服务器建立三次握手协议后,客户端又发来RST,之后服务器调用accept就会返回ECONNABORTED,这是软错误,不需要退出程序,只需要忽略它再次调用accept获取下一个可连接套接字。(5.11)

6. 读写函数:

网络IO操作有下面几组:

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

最常用的是read/write。对于read返回值大于0表示读取成功返回实际所读的字节数,0表示文件结束,对方调用了close;返回小于0的数表示出现了错误。如果错误是EINTR说明读是由中断引起的。write返回值大于0表示写成功,写了部分或者全部数据;返回值小于0表示出现了错误。如果错误是EINTR表示写时出现了中断错误。

7. close:

int close(int sockfd);

(1)作用:将套接字句柄引用计数减1,如果引用计数降为0,则将套接字标记为关闭,并立即返回到调用进程。而Tcp协议栈则尝试发送已排序等待发送到对端的任何数据,之后发送FIN分节,接收端(协议栈)收到后传递给应用程序一个文件结束符。之后接受端发送ACK和FIN。之后发送端再次发送ACK。
(2)标记为关闭的套接字之后不能再被进程调用,既不能再用于read和write

8. shutdown:

shutdown没有句柄引用计数的概念,它的作用是立即向对端发送FIN,并且shutdown之后的套接字只不能用于写,但可用于读

9. getsockname getpeername

int getsockname(int sockfd,struct sockaddr* localaddr,socklen_t* addrlen);
int getpeername(int sockfd,struct sockaddr* peeraddr,socklen_t* addrlen);

(1)getsockname:返回与某个套接字关联的本地协议地址(IP地址和端口号和地址族)
(2)getpeername:返回与某个套接字关联的对端协议地址(IP地址和端口号和地址族)
(3)需要这两个函数的理由:
a. 在没有调用bind或以端口号0调用bind的客户程序中,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和端口号。
b. 用通配IP地址调用bind的服务器程序中,getsockname用于返回内核赋予该连接的本地IP地址。注意:传入的套接字描述符必须是已连接套接字描述符,而不是监听套接字描述符
c. 如果不知道传入的套接字地址具体是哪个地址族,可以传入sockaddr_storage,该结构能承载系统支持的任何套接字地址结构的空间大小。

int sockfd_to_family(int sockfd)
{
    struct sockaddr_storage ss;
    socklen_t len;
    len = sizeof(ss);
    if(getsockname(sockfd,(sockaddr*)&ss,&len) < 0;
        return -1;
    return (ss.ss_family);
}

10. 简单的echo 服务端和客户端程序举例

服务端程序server.cpp

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>   //struct sockaddr_in
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define LISTEN_BACKLOG 50
#define MAXLINE 2048
#define handle_error(msg)\\
    do { perror(msg); exit(EXIT_FAILURE); } while(0)
int main()
{
    int listenfd,peerfd;
    struct sockaddr_in sockaddr;    
    char buffer[MAXLINE] = {0};
    if ((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        handle_error("create socket error");        
    }
    memset(&sockaddr,0,sizeof(struct sockaddr_in));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    sockaddr.sin_port = htons(7777);
    if(bind(listenfd,(struct sockaddr*)&sockaddr,sizeof(struct sockaddr_in)) == -1)
    {
        handle_error("bind");
    }
    if(listen(listenfd,LISTEN_BACKLOG) == -1)
    {
        handle_error("listen error");
    }
    while(1)
    {       
        int readn;
        struct sockaddr_in peeraddr;
        socklen_t addrlen = sizeof(peeraddr);
        char ip[20]={0};
        memset(&peeraddr,0,sizeof(peeraddr));
        if((peerfd = accept(listenfd,(struct sockaddr*)&peeraddr,&addrlen)) == -1)
        {
            handle_error("accept");
        }
        inet_ntop(AF_INET,&peeraddr.sin_addr,ip,20);
        printf("peeraddr ip:%s port:%d\\n",ip,ntohs(peeraddr.sin_port));
        readn = read(peerfd,buffer,MAXLINE);
        if(readn > 0)
        {
            buffer[readn] = '\\0';
            printf("write:%s",buffer);            
            write(peerfd,buffer,readn);
            close(peerfd);
        }   
        else if(readn == 0)
        {
            close(peerfd);
        }   
        else
        {
            close(peerfd);
            perror("read");
        }   

    }
    close(listenfd);
    return 0;
}

客户端程序client.cpp,客户端使用./client ip运行,每发送一次数据就结束程序

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> 
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define MAXLINE 2048
#define handle_error(msg)\\
    do { perror(msg); exit(EXIT_FAILURE); } while(0)
 int main(int argc,char* argv[])
 {
    int fd;
    struct sockaddr_in serveraddr;
    char buffer[MAXLINE] = {0};
    if(argc != 2)
    {
        printf("usage:client ip\\n");
    }

    fd = socket(AF_INET,SOCK_STREAM,0);
    if (fd == -1)
    {
        handle_error("create socket error");        
    }

    memset(&serveraddr,0,sizeof(struct sockaddr_in));    
    serveraddr.sin_family = AF_INET;
    if(inet_pton(AF_INET,argv[1],&serveraddr.sin_addr) <= 0)
    {
        handle_error("inet_pton");
    }   
    serveraddr.sin_port = htons(7777);
    if(connect(fd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr_in)) == -1)
    {
        handle_error("connect error");
    }

    int n = read(0,buffer,1024);       //读控制台:调fgets或 read(0,)
    if( write(fd,buffer,n) < 0)
    {
        handle_error("write");
    }

    close(fd);
    return 0;
 }

11. 并发服务器

(1)fork返回值>0的,表示当前进程是父进程,fork返回的是子进程的pid。fork返回值=0的,表示当前进程是子进程,子进程可调用getppid获取父进程的pid。
(2)当服端 务一个客户请求可能花费较长时间时,我们并不希望整个服务器被整个客户端长期占用,而是希望同时服务多个客户。Unix中编写并发服务器最简单的办法就是fork一个子进程来服务每个客户。
(3) 实现过程:当通过accept获取一个客户请求,然后fork一个子进程,子进程首先关闭监听监听套接字,并处理客户请求。父进程则关闭连接套接字,并再次调用accept获取下一个客户请求。
(4)使用fork对关闭套接字的处理:父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享。父进程调用accept之后调用fork,所接受的监听套接字和已连接套接字则由父子进程共享。通常,子进程关闭监听套接字,接着读写这个已连接套接字;父进程不要忘记关闭这个已连接套接字。原因见下面注释。
(5)fork的子进程处理完连接套接字描述符后,不要忘记调用exit退出进程,否则会执行到父进程的代码。
(5)典型的并发服务器程序轮廓:

pid_t pid;
int listenfd,connfd;
listenfd = Socket(...);
Bind(listenfd, ...);
Listen(listenfd,LISTENQ);
for(;;)
{
    connfd = Accept(listenfd, ... );
    if( (pid = Fork()) == 0)
    {
         Close(listenfd);  //因为exit会终止进程,而进程终止处理的部分工作就是关闭所有由内核打开的描述符,所有close listenfd可写可不写
         doit(connfd);
         Close(connfd);
         exit(0);  //不要忘记调用exit,来关闭该进程
     }
     Close(connfd);   //不要忘记close connfd,因为父进程不会用到connfd, close connfd不一定会真的关闭进程,它只是把进程的引用计数减一。如果父进程不关闭connfd,即使子进程close connfd也不会真正关闭connfd,导致连接一直打开着。而且这将导致父进程耗尽可用描述符,因为任何进程在任何时刻可拥有的打开着的描述符通常是有限的。
}

12. 总结:

(1)传入bind的套接字地址IP和端口号不要忘记转换成网络字节序
(2)bind指定的IP地址可以为通配IP地址,表示内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UPD)时才选择一个本地IP地址。对于IPv4用INADDR_ANY指定,对于IPv6用in6addr_any指定。bind指定的端口号可以为0,表示内核自己分配端口
(3)若connect失败后则该套接字不再使用,如果需要再次调用connect时,必须close当前的套接字都重新调用socket
(4)accept返回的套接字地址是网络字节序,如果对accept返回的套接字地址不感兴趣,cliaddr和addrlen可以设置为NULL;如果需要读取accept返回的套接字地址,需要先从网络字节序转换从主机字节序

(5)close只是将套接字句柄引用计数减1,如果引用计数降为0,才将套接字标记为关闭,之后进程不能再对close的套接字进行任何调用
(6)对套接字标记为关闭后,Tcp协议栈则尝试发送已排序等待发送到对端的任何数据,之后发送FIN分节,接收端(协议栈)收到FIN后传递给应用程序一个文件结束符通知应用程序收到了对端的FIN,之后两端发送的网络终止序列不在累述
(7)getsockname和getpeername用于返回与某个套接字关联的本地和对端协议地址(IP地址 端口号 地址族)
(8)使用fork实现并发服务器:子进程不要忘记关闭监听套接字,接着读写这个已连接套接字;父进程不要关闭这个已连接套接字;fork的子进程处理完连接套接字描述符后,不要忘记调用exit退出进程,否则会执行到父进程的代码。典型的并发服务器程序轮廓见11.(5)

以上是关于unix网络编程笔记的主要内容,如果未能解决你的问题,请参考以下文章

读书笔记《Unix编程艺术》三

UNIX网络编程学习笔记2 需要用到的一些字节操纵和格式转换函数

UNIX网络编程笔记—传输层协议

unix网络编程笔记

unix网络编程笔记

《理解 Unix 进程》笔记-1