zoukankan      html  css  js  c++  java
  • unix网络编程(1)---客户端-服务器第一版

    个人认为《Unix网络编程》前4章可以好好看几遍,不用先着急编程。另外作者提供的源码封装太重,不如自己基于原始库函数编写客户端以及服务器,目前一些开源的项目也都是基于这些基础库函数的。

    在了解了前四章的主要知识点后,比如socket、bind、connect、listen、accept等函数后,对网络编程有了一定的了解后,就可以参考第5章来写自己的客户端和服务器了。对于新手来说这里比较抽象,而且很多地方绕来绕去容易绕晕,需要重复看多次,再看后边的章节。

    这篇文章我就从第5章开始,仿照书上的demo写一个可以直接在单机上运行的cli-ser程序。

    以下是server的对应程序:server.c

     1 #include <unistd.h>
     2 #include <stdlib.h>
     3 #include <errno.h>
     4 
     5 #define MAXLINE 1024
     6 
     7 extern int errno;
     8 
     9 void str_echo(int);
    10 
    11 int main() {
    12     int sockfd;
    13     sockfd = socket(AF_INET, SOCK_STREAM, 0);
    14 
    15     struct sockaddr_in servaddr, cliaddr;
    16     bzero(&servaddr, sizeof(servaddr));
    17     servaddr.sin_family = AF_INET;
    18     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    19     servaddr.sin_port = htons(7070);
    20     bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    21     listen(sockfd, 1024);
    22 
    23     for (;;) {
    24         int connfd, childPid;
    25         socklen_t len = sizeof(cliaddr);
    26         connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    27 
    28         if ((childPid = fork()) == 0) {
    29             close(sockfd);
    30             printf("connected with client.
    ");
    31             str_echo(connfd);
    32             exit(0);
    33         }
    34     }
    35 
    36     printf("server end!
    ");
    37     return 0;
    38 }
    39 
    40 void str_echo(int sockfd) {
    41     ssize_t n;
    42     char buf[MAXLINE];
    43 
    44 again:
    45 
    46     while ((n = read(sockfd, buf, MAXLINE)) > 0) {
    47         printf("n:%ld
    ", n);
    48         write(sockfd, buf, n);
    49         bzero(buf, MAXLINE);
    50 
    51         if (n < 0 && errno == EINTR) {
    52             goto again;
    53         } else if (n < 0) {
    54             printf("str_echo:read error
    ");
    55         }
    56     }
    57 }

    编译:gcc server.c -o server

    这里先列下经常用到的网络字段类型:

    代码流程:

    1、申请socket

    服务器首先申请socket,socket类似于再Unix系统上打开一个文件,会返回一个文件标识号用来标识当前打开的文件。

    socket需要引用<sys/socket.h>头文件

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

    family:对应的是协议族,ipv4:AF_INET   ipv6:AF_INET6

    type:套接字类型,tcp对应SOCKET_STREAM(数据流)

    protocol:协议类型,这里我们用0,内核会根据family和type选择默认的协议,对于family:AF_INET,type:SOCK_STREAM,默认的协议是tcp

    2、端口绑定

    一般服务器启动一个服务进程会开启某个端口的监听工作,所以一般的服务器进程需要绑定固定的端口号,也就是该进程对应的socket需要绑定到某一个端口号。对于多网卡的服务器,会对应多个ip,当然也可以绑定固定的ip,我们这里不进行绑定 ,使用通配地址(ipv4:INADDR_ANY,ipv6:IN6ADDR_ANY_INIT),此处的端口或ip绑定用的函数是bind

    #include <sys/socket.h>

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

    sockfd:监听套接字,对于服务器来说,即调socket返回的套接字

    myaddr:套接字结构体,我们一般会先申请一个sockaddr_in结构的套接字,通过bzero函数(string.h的一个函数)进行结构体初始化为0,分别对family,ip,port填值,然后用sockaddr强制类型转化进行调用,具体的可以参考书中bind函数使用;

    addrlen:为套接字结构体长度

    3、套接字端口监听

    目前已经在申请好的套接字上进行了监听ip及port的初始化,那么可以内核开始按照我们初始化的信息进行监听了,即调用listen函数,内核会申请一个队列用于存放未完成连接以及已完成连接的套接字,如下图

    映射到tcp的三次握手,如下图:

    4、与客户端建立连接

    下边我们会进入一个无限循环,会一直处理client发来的tcp链接,accept为阻塞函数,如果没有客户端连接,这个函数会被阻塞,也就是程序会在这里停止,知道有client建立了tcp连接accept才返回,accept返回也就说明,此时已经建立好一条tcp连接通路,下边我们的服务器会在这条通路上进行数据的发送与接收,至于接收后会怎么处理,以及返回客户端什么数据,就属于服务器自己的业务需求了。我们这里会fork一个子进程进行这些逻辑的处理。为什么要建立子进程呢?我们的服务器进程是并发的服务器,如果accept后,进程开始处理业务逻辑,那么其他的client需要等待这条tcp完成逻辑处理后,才能进入下一次循环。所以我们新建子进程专门用于逻辑的处理,至于父进程就专门负责accept,建立新的链接,这样多个client发起与服务器的tcp链接,服务器主进程可以一直循环accept建立连接,然后fork子进程进行后续处理,这样我们就实现了简单的并发服务器,可以同时与多个client建立tcp连接。

    #include <sys/socket.h>

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

    这里有一点需要注意,addrlen使用的是指针,这是由于addrlen的入参会被内核使用到,已提醒读取cliaddr的长度,另外,内核会写回cliaddr,这也防止内存溢出,并且写入多少这个数,内核还会写回addrlen,这里一个参数做了多个事情,所以用了值—参数这种指针传参。

    #include <unistd.h>

    pid_t fork(void);

    创建子进程,对于父进程返回值为子进程的进程id,对于子进程返回0。

    对于server中用到的read和write函数,参考Unix高级编程中的相关知识。

    以下是client代码:client.c

     1 #include <sys/socket.h>
     2 #include <netinet/in.h>
     3 #include <stdio.h>
     4 #include <string.h>
     5 #include <arpa/inet.h>
     6 #include <unistd.h>
     7 #include <unistd.h>
     8 
     9 #define MAXLINE 1024
    10 
    11 void str_cli(FILE *, int);
    12 
    13 int main() {
    14     int sockfd;
    15     const char *ip = "127.0.0.1";
    16     in_port_t port = 7070;
    17 
    18     int i = 0;
    19     sockfd = socket(AF_INET, SOCK_STREAM, 0);
    20     struct sockaddr_in cliaddr;
    21     bzero(&cliaddr, sizeof(cliaddr));
    22     cliaddr.sin_family = AF_INET;
    23     inet_aton(ip, &cliaddr.sin_addr);
    24     cliaddr.sin_port = htons(port);
    25 
    26     int ret = connect(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
    27     str_cli(stdin, sockfd);
    28 
    29     return 0;
    30 }
    31 
    32 void str_cli(FILE *fp, int sockfd) {
    33     char sendline[MAXLINE], recvline[MAXLINE];
    34 
    35     while (fgets(sendline, MAXLINE, fp) != NULL) {
    36         write(sockfd, sendline, strlen(sendline));
    37 
    38         if (read(sockfd, recvline, MAXLINE) == 0) {
    39             printf("server terminated prematurely
    ");
    40         }
    41         fputs(recvline, stdout);
    42         bzero(recvline, MAXLINE);
    43     }
    44 }

    编译:gcc client.c -o client

    客户端的流程:

    1、建立套接字

    2、发起tcp连接

    #include <sys/socket.h>

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

    connect也是阻塞函数,tcp连接成功后返回0。

    到这里我们完成了一个超级简单的服务器-客户端程序的开发。之后我们会对这个程序不断完善。

    下文:

    本篇中写的服务器,fork的子进程执行完直接调exit了,我们知道子进程结束后但是父进程没有回收其对应的空间(进程号等),随着子进程的不停申请,但得不到释放,内核会内存泄露,也就是变成了僵尸进程。下一篇,我们引入对子进程的空间释放解决这个问题。

  • 相关阅读:
    Kvm virsh
    lvs tunl
    django表单使用
    django上传图片
    django框架admin使用
    django模板的使用
    django数据库操作
    django数据库交互
    django数据库中
    django之类视图
  • 原文地址:https://www.cnblogs.com/wangtengxiang/p/10424343.html
Copyright © 2011-2022 走看看