Linux系统编程(八)网络基础与Socket通信


分层模型结构

OSI七层模型  

物理层:主要定义物理设备标准(如网线接口类型、光纤的接口类型、传输介质的传输速率等),主要用于传输比特流

数据链路层:定义了如何让格式化数据以帧未单位进行传输,以及如何控制对五列戒指的访问,提供错误检测和纠正,确保数据可靠传输

网络层:阿紫位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择

传输层:定义了一些传输数据的协议和端口号(www、80),主要将下层接收到的数据进行分段和传输,到达目的地址后再进行重组

会话层:通过传输层(传输端口和接收端口)建立数据传输的通路

表示层:确保一个系统的应用层所发的信息可以被另一个系统的应用层读取

应用层:为用户的应用程序提供网络服务

OSI模型 TCP/IP模型
应用层 应用层
表示层  
会话层  
传输层 传输层
网络层 网络层
数据链路层 链路层
物理层  

TCP/IP四层模型

应用层

为用户的应用提供服务并支持网络访问

转化数据格式

负责管理网络中计算机之间的通信、提供传输层不具备的连接功能

  http

  ftp

  nfs

  ssh

  telnet

传输层

为应用程序提供接口,提供网络访问的途径

提供可以多路复用和多路分解的功能

数据的错误检查和流量控制

  TCP:面向连接

  UDP:无连接

     16位源端口号、16位目的端口号

      IP地址可以在网络环境中唯一标识一台主机

      端口号:可以在网络的一台主机上,标识一个进程

网络层

主要解决数据如何由一个计算机的IP路由到目标计算机的过程规范

  IP:TTL(time to live),32位4byte,源IP、目的IP

  ICMP

  IGMP

链路层

管理物理网络

  以太网帧协议:根据mac地址完成数据包传输

  ARP:根据IP地址获取mac地址

看这儿我就有个问题OSI模型和TCP/IP模型有什么关联?

查阅资料感觉以下说法更容易理解:OSI七层模型是理论上的分层方式,TCP/IP四层模型是实践过程中的分层模型

网络套接字

C/S模型:client-server

  缓存大量数据、协议选择灵活,速度快    /    安全性稍差

B/S模型:browser-server

  不能缓存大量数据,严格遵守http    /   安全性高、跨平台、开发工作量小

套接字可以说是TCP/IP模式的实现,是在TCP/IP之上进行的封装,是一个C/S模型。

通信过程中套接字是成对出现的,一端的发送缓冲对应对端的接收缓冲。

网络字节序

大端存储(网络存储):高位存低地址,低位存高地址

小端存储(主机存储)

网络字节序和主机字节序转换

#include 
uint32_t htonl(uint32_t hostlong);     // 本地转网络 host to network long(IP)
uint16_t htons(uint16_t hostshort);   // 本地转网络 port
uint32_t ntohl(uint32_t netlong);    // 网络转本地 IP
uint16_t ntohs(uint16_t netshort);   // 网络转本地 port

IP地址转换函数

#include 
int inet_pton(int af, const char *src, void *dst);   // 点分十进制转换网络字节序
//af: AF_INET、AF_INET6
//src:点分十进制IP地址
//dst:网络字节序IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 网络字节序转本地字节序

sockaddr

bind传参为sockaddr,但是现在用的sockaddr_in / sockaddr_un/ sockaddr_in6,需要强转为sockaddr 

           struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */ 地址类型
               in_port_t      sin_port;   /* port in network byte order */ 端口
               struct in_addr sin_addr;   /* internet address */ ip地址
           };

           /* Internet address. */
           struct in_addr {
               uint32_t       s_addr;     /* address in network byte order */  网络字节序的ip地址
           };

相关内容可以通过 man 7 ip 来查看manpage

man bind 可以查看本地套接字通信例子

其中ip地址可以用两种:

int dest;
inet_pton(AF_INET, "192.168.0.1", void*(&dst));
// or
addr.sin_addr.s_addr = htonl(INADR_ANY)   // 取出系统中有效的任意IP

socket函数

1、socket

#include 
int socket(int domain, int type, int protocol);    // 创建套接字

domain:AF_INET、AF_INET6、AF_UNIX(套接字协议类型)

type:SOCK_STREAM(流式协议)、SOCK_DGRAM(道式协议)

protocol:代表协议 0,如果是stream->TCP,DGRAM->UDP

2、bind ,给socket绑定一个地址结构(IP+port)

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

socklen_t:地址结构的大小

3、listen , 设置同时与服务器建立连接的上限数,最大值为128

int listen(int sockfd, int backlog);

4、accept , 阻塞等待客户端建立连接,如果成功则返回一个与客户端成功连接的socket文件描述符

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

第二个参数sockaddr为传出参数,传出成功与服务器建立连接的那个客户段的地址结构(IP+port)

第三个参数socklen_t为传入传出参数,传出客户端addr实际大小

5、connect , 与socket服务器建立连接

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

第二个参数为传入参数,为服务器地址结构

第三个参数为服务器地址结构长度

网络套接字demo

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERVER_PORT 9352

// server
int main(int argc, char** argv)
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("create socket fail");
        exit(1);
    }

  // 构建server sockaddr_in
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);       // 这里要把端口转换为网络字节序
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 使用任意有效IP,另一种写法在client中

    bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

    listen(fd, 10);

    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // accept阻塞等待连接,连接成功返回一个用于连接的文件描述符,同时获得client端的sockaddr_in
    int cfd = accept(fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if(cfd == -1)
    {
        perror("accept error");
        exit(1);
    }
    
    char client_ip[20];
    // 打印出client的IP地址,以及client端的Port,也可以使用write打印到标准输出上
    printf("client ip:%s, port:%d\n", inet_ntop(AF_INET, (void*)&client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)), client_addr.sin_port);

    char buf[BUFSIZ];
    int n = 0;
    while(1)
    {
     // 从client端读取数据
        n = read(cfd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, n);

        for(int i = 0; i < n; i++)
            buf[i] = toupper(buf[i]);
        // 写入到client
        write(cfd, buf, n);
    }

    close(cfd);
    close(fd);
}

接下来看client的写法:

#include 
#include 
#include 
#include 
#include 
#include 
#include <string.h>

#define SERVER_PORT 9352

int main(int argc, char** argv)
{ 
    // client端同样也要创建一个socket,这个socket就是用来与server端通信的fd
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    
   // 构建要连接的server的sockaddr_in
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);   // 要连接的端口
    inet_pton(AF_INET, "192.168.154.128", (void*)&server_addr.sin_addr.s_addr);   // 要连接的ip地址,inet_pton将点分十进制ip转化为网络字节序ip地址
    
  // 连接server,这里的返回值不是fd,成功返回0,失败返回-1,
    int ret = connect(cfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if(ret != 0)
    {
        perror("connect error");
        exit(1);
    }

    char buf[1024];
    char write_buf[20] = "hello socket\n";
    while(1)
    {
        write(cfd, write_buf, strlen(write_buf));
        int n = read(cfd, buf, 1024);
        write(STDOUT_FILENO, buf, n);
        sleep(1);
    }
    return 0;
}

一开始连接时发现connection refused,因为server端的端口有问题,换了端口就OK了。manpage中提到Internet domain sockets can be found in getaddrinfo(3)

本地套接字demo

对比网络套接字,有以下几点不同:

1、domain 使用AF_UNIX / AF_LOCAL

2、地址结构变成 sockaddr_un,可以通过man 7 unix来查到

struct sockaddr_un {
       sa_family_t sun_family;               /* AF_UNIX */
       char        sun_path[108];            /* pathname */
};

3、bind绑定不一样

size_t offsetof(type, member);
bind(sfd, (struct sockaddr*)&service_addr, offsetof(struct sockaddr_un, sun_path) + strlen(SOCK_PATH))

因为sun_path长度为108,我们要获取到service_addr的真实长度,第一个元素为2字节+第二个元素的偏移量开始的sun_path长度

bind函数调用调用成功,会创建一个socket文件,为保证bind成功,通常在bind之前,可以使用unlink删除SOCK_PATH

4、客户端不能依赖隐式绑定,通信建立过程中,需要创建初始化两个地址结构,client_addr用于bind,server_addr用于connect

接下来尝试写一个UNIX本地socket通信的demo:

#include 
#include 
#include 
#include 
#include 
#include <string.h>
#include 
#include 

#define SOCK_PATH "service.socket"

// Server
int main(int argc, char** argv)
{
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("create socket fail");
        exit(1);
    }

    struct sockaddr_un server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, SOCK_PATH);
    
    int len = offsetof(struct sockaddr_un, sun_path) + strlen(server_addr.sun_path);

    unlink(SOCK_PATH);
    int ret = bind(fd, (struct sockaddr*)&server_addr, len);
    if(ret == -1)
    {
        perror("bind error");
        exit(1);        
    }
    listen(fd, 10);

    struct sockaddr_un client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int cfd = accept(fd, (struct sockaddr*)&client_addr, &client_addr_len);
    
    printf("client addr = %s\n", client_addr.sun_path);
    if(cfd == -1)
    {
        perror("accept error");
        exit(1);
    }
    
    char buf[BUFSIZ];
    int n = 0;
    while(1)
    {
        n = read(cfd, buf, sizeof(buf));
        write(STDOUT_FILENO, buf, n);

        for(int i = 0; i < n; i++)
            buf[i] = toupper(buf[i]);

        write(cfd, buf, n);
    }

    close(cfd);
    close(fd);
}
#include 
#include 
#include 
#include 
#include 
#include <string.h>
#include 
#include 

#define CLI_SOCK_PATH "client.socket"
#define SER_SOCK_PATH "service.socket"

// Client
int main(int argc, char** argv)
{
    int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
    
    struct sockaddr_un client_addr;
    memset(&client_addr, 0, sizeof(client_addr));
    client_addr.sun_family = AF_UNIX;
    strcpy(client_addr.sun_path, CLI_SOCK_PATH);

    int len = offsetof(struct sockaddr_un, sun_path) + strlen(client_addr.sun_path); 
    printf("client_addr.sun_path  = %s, len = %d\n", client_addr.sun_path, len);
    unlink(CLI_SOCK_PATH);
    int ret = bind(cfd, (struct sockaddr*)&client_addr, len);
    if(ret != 0)
    {
        perror("bind error");
        exit(1);
    }
    ///////////////////////////////////////////////
    
    struct sockaddr_un server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, SER_SOCK_PATH);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(server_addr.sun_path);

    ret = connect(cfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if(ret != 0)
    {
        perror("connect error");
        exit(1);
    }
    int cnt = 0;
    char buf[1024];
    char write_buf[20] = "hello socket\n";
    while(cnt < 10)
    {
        write(cfd, write_buf, strlen(write_buf));
        int n = read(cfd, buf, 1024);
        write(STDOUT_FILENO, buf, n);
        sleep(1);
        cnt++;
    }
    return 0;
}

执行之后看到目录下会多出server.socket 和 client.socket两个socket文件

常见读取方法

ssize_t Readn(int fd, void *vptr, size_t n)
{
    size_t nleft = n;
    ssize_t nread;
    char *ptr = vptr;

    while(nleft > 0){
        if((nread = read(fd, ptr, nleft)) < 0){
            if(errno == EINTR)
                nread = 0;
            else
                return -1;
        }
        else if(nread == 0)
            break;
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
    size_t nleft = n;
    ssize_t nwritten;
    const char *ptr = vptr;
    
    while(nleft > 0){
        if((nwritten = write(fd, ptr, nleft)) <= 0)
            if(nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else
                return -1;
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

static ssize_t my_read(int fd, char *ptr)
{
    static int read_cnt = 0;
    static int *read_ptr;
    static char read_buf[100];

    if(read_cnt <= 0){
again:
        if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0){
            if(errno == EINTR)
                goto again;
            return -1;
        }
        else if(read_cnt == 0){
            return 0;
        }

        read_ptr = read_buf;
    }

    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
    ssize_t n, rc;
    char c, *ptr;
    ptr = vptr

    for(n = 1; n < maxlen; n++){
        if((rc = my_read(fd, &c)) == 1){
            *ptr++ = c;
            if(c == '\n')
                break;
        }
        else if(rc == 0){
            *ptr = 0;
            return n-1;
        }
        else{
            return n-1;
        }
    }
    *ptr = 0;

    return n;
}

并发服务器

多进程并发服务器

主要思想就是server accept成功之后fork开启一个新进程来与client通信

// server

#include #include #include #include #include #include #include #include #include <string.h> #include #define SRV_PORT 9999 void signal_handler(int signo) { while(waitpid(0, NULL, WNOHANG) > 0); return; } int main(int argc, char** argv) { int lfd, cfd; struct sockaddr_in srv_addr, cli_addr; int addr_len; pid_t pid; char rbuf[1024]; char wbuf[1024]; char ip_buf[20]; struct sigaction act; act.sa_handler = signal_handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); int ret = sigaction(SIGCHLD, &act, NULL); memset(&srv_addr, 0, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(SRV_PORT); srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); lfd = socket(AF_INET, SOCK_STREAM, 0); ret = bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr)); if(ret == -1) { perror("bind error"); exit(1); } ret = listen(lfd, 128); if(ret == -1){ perror("bind error"); exit(1); } addr_len = sizeof(cli_addr); while(1) { cfd = accept(lfd, (struct sockaddr*) &cli_addr, (socklen_t*)&addr_len); if(cfd == -1) { if(errno == EINTR) continue; perror("accept error"); exit(1); } pid = fork(); if(pid == 0) { close(lfd); break; } else if (pid > 0) { close(cfd); continue; } else { perror("fork error"); exit(1); } } if(pid == 0) { ssize_t n; while(1) { n = read(cfd, rbuf, sizeof(rbuf)); if(n == 0) { close(cfd); exit(1); } fprintf(stdout, "PID : %d, get client ip : %s, message : %s", getpid(), inet_ntop(AF_INET, (void*)&cli_addr.sin_addr.s_addr, ip_buf, sizeof(cli_addr)), rbuf); sprintf(wbuf, "server id : %d response\n", getpid()); write(cfd, wbuf, strlen(wbuf)); } } }

client端和上面的网络套接字中的demo相同

可以通过netstat查看client/server状态

$ netstat -apn | grep 9999

  tcp 0 0 0.0.0.0:9999      0.0.0.0:*        LISTEN        20770/./server
  tcp 0 0 127.0.0.1:9999    127.0.0.1:44278  ESTABLISHED   20774/./server
  tcp 0 0 127.0.0.1:44278   127.0.0.1:9999   ESTABLISHED   20773/./client

read返回值

1. > 0,实际读到的字节数

2. = 0,已经读到结尾,对端已经关闭

3. -1,  应该进一步判断errno值

    errno = EAGAIN 或者 EWOULDBLOCK,设置了非阻塞模式,数据没有到达。socket模式下默认是非阻塞,可以在manpage中查看具体内容

    errno = EINTR ,被系统调用终端

    errno = 其他 ,异常

多线程并发服务器

主要思想就是server accept成功之后开启一个新线程来与client通信,这里暂时不写demo

端口复用

之前主动关闭server端时,server会进入到TIME_WAIT状态,并没有close,再次执行server会出现端口被占用的情况(bind error:Address already in use),需要等待2MSL到端口被释放

setsockopt,添加在bind之前

int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt))