Linux 下C语言简单echo server的实现

Linux 下C语言简单echo server的实现

本文为 高性能网络编程学习 系列文章的第0篇

本文目的为讲述简单的echo server的实现, echo server的含义如字面意思, 客户端发送什么数据,服务器就返回什么数据作为响应,本系列文章的目的不在于实现复杂的服务器端业务逻辑,在于学习如何开发出高性能的网络通信程序,因而所有的代码均为echo server的实现

Linux下用C进行网络通信的话, 需要使用socket, 即Linux socket interface, socket是一种IPC(Inter Process Communication进程间交互)的方式,同时socket还可以允许跨host,即在不同的主机上的进程之间进行通信,在Linux上,所有的IO操作都是通过对文件的写入读出实现的,socket也作为一种文件存在,可以通过read, write进行IO

客户端我们采用的是一个benchmark程序,即1000个client,每一个client发送1000个请求,客户端为了简洁和快速编写,使用golang开发,之后所有的server都使用这个benchmark进行测试,代码如下

package main  
                                                                                                         
import (
        "flag"
        "fmt"
        "io"
        "net"
        "sync"
        "time"
)
 
var wg sync.WaitGroup
 
var okCliCnt = 0
var okIPCCnt = 0
 
var mu sync.Mutex
 
var (
        ConnType   = "tcp"
        ConnAddr   = ""
        ClientCnt  = 1000
        RequestCnt = 1000
)
 
func init() {
        flag.StringVar(&ConnAddr, "S", "127.0.0.1:5210", "specify the address to connect")
        flag.IntVar(&ClientCnt, "cno", 1000, "number of clients to request")
        flag.IntVar(&RequestCnt, "rno", 1000, "number of request per client perform")
        flag.Parse()
}
 
func run(id int) {
        //mu.Lock()
        //defer mu.Unlock()
        defer wg.Done()
        cli, err := net.Dial(ConnType, ConnAddr)
        if err != nil {
                fmt.Println("Error", err)
                fmt.Printf("[No %d]Error when establishing connection, client quit\n", id)
                return
        }
        defer cli.Close()
        buf := make([]byte, 20)
        for i := 0; i < RequestCnt; i++ {
                _, err := cli.Write([]byte("Client Hello"))
                if err != nil {
                        fmt.Println("Error", err)
                        fmt.Printf("[No %d]Error when writing to server, client quit\n", id)
                        break
                }
                _, err = cli.Read(buf)
                if err != nil && io.EOF != err {
                        fmt.Println("Error", err)
                        fmt.Printf("[No %d]Error when reading from server, client quit\n", id)
                        break
                }
                okIPCCnt++
        }
        okCliCnt++
}

客户端程序不进行赘述,我们下面实现服务器端,通过socket监听一个端口并且与客户端进行读写交互有以下几个关键步骤

  1. socket创建
  2. 将socket绑定到指定的地址和端口
  3. 将socket变为listen状态
  4. 通过accept接受来自客户端的链接
  5. read/write与客户端进行交互

本例我们实现的是一个单进程的echo server, 下面是server的代码

/*************************************************************************
  > File Name: server.c
  > Author: VOID_133
  > A very simple tcp echo server illustrate use of socket
  > Ver1 only accept single connection, no multiple connection allowed
  > Mail: ###################
  > Created Time: Wed 21 Dec 2016 04:03:25 PM HKT
 ************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<string.h>
#include<sys/errno.h>

#define MAX_BUF_SIZE 1000

int main(int argc, char** argv) {
    int sockfd, clifd;

    //sockaddr_in is used to describe internet(IPV4) socket address
    struct sockaddr_in server_in_addr;
    char buf[MAX_BUF_SIZE];
    int ret = 0;

    //create the socket flide
    //AF_INET is the address family for internet, SOCK_STREAM means TCP connections
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("socket()");
        exit(-1);
    }
    //Set all bytes to zero
    bzero(&server_in_addr, sizeof(server_in_addr));
    server_in_addr.sin_family = AF_INET;                                                                                [130/1956]

    //inet_aton convert string based address to binary data
    //Do not use inet_addr, for more info see man page
    ret = inet_aton("0.0.0.0", &server_in_addr.sin_addr);

    //You can also choose the code below to let socket
    //listen to all interface
    //code: server_in_addr.sin_addr.s_addr =  htonl(INADDR_ANY);

    //Convert unsigned short to on-wire data
    server_in_addr.sin_port = htons(8080);
    if(ret < 0) {
        perror("inet_aton()");
        exit(-1);
    }

    //Let socket bind to the server address and port
    ret = bind(sockfd, (const struct sockaddr *)&server_in_addr,sizeof(struct sockaddr_in));
    if(ret < 0) {
        perror("bind()");
        exit(-1);
    }

    //Listen for incoming connections
    ret = listen(sockfd, 50);
    if(ret < 0) {
        perror("listen()");
        exit(-1);
    }
    while(1) {
        //accept will block until a client connect to the server
        clifd = accept(sockfd, NULL, NULL);
        if(clifd < 0) {
            perror("accept()");
            exit(-1);
        }
        printf("Connect fd = %d\n", clifd);
        memset(buf, 0, sizeof(buf));

        //read from the client until client close/or send EOF
        while((ret = read(clifd, buf, (size_t)MAX_BUF_SIZE)) && ret != EOF) {
            if(ret < 0) {
                perror("read()");
                exit(-1);
            }
            printf("Get data %s\n", buf);

            //write back to the client
            ret = write(clifd, buf, strlen(buf) * sizeof(char));
            if(ret < 0) {
                perror("write()");
                exit(-1);
            }
        }
        close(clifd);
    }
    return 0;
}

如果是第一次写socket通信的代码,有几个地方容易卡住,下面把上述代码中几个比较容易出错和忘记的地方注明一下

整体流程

  • Create socket的时候使用socket()函数,具体用法查看man page,关于domain, protocol family的指定,man page中说的是“implementation-defined”想要找到所有的定义,Linux下需要查看 bits/socket.h文件
  • sockaddr_in 这个结构体用于表示internet (IPV4)的address, 这里使用的是sockaddr_in而不是sockaddr
  • 使用bind的时候注意addr结构体为sockaddr*,我们使用的是sockaddr_in需要进行类型转换,不用担心转换数据丢失或者错乱,这些结构体在实现的时候就已经假定用户使用时一定会用到强制类型转换的
  • bind之后,我们通过listen,使这个socket变为可以接受外界链接的状态
  • listen之后通过accept接受client的链接,每一个client都会产生一个新的文件以及fd,只需要对这个fd进行读写,就可以实现client和server的交互了
  • accept的时候需要注意一点,accept的参数中传递的socket应该为用于listen的socket,这个socket可以accept多个连接,accept返回的文件描述符对应的文件虽然也是一个socket,不过不可以用于accept多个连接,应该再这个链接断开时关闭掉相应的fd

关于sockaddr_in的参数以及初始化

  • 对于sockaddr_in结构体的addr的指定,指定其监听所有网卡的最常用的方式是 htonl(INADDR_ANY) 注意这里一定要用htonl来进行不同机器的兼容性转换,网络上的机器的架构,数据表示均不同,为了让数据能够在各个机器上被正确解读,我们需要用到几个宏(函数)对数据进行转换,具体参考man page byteorder,  这里的数据INADDR_ANY为一个长整型,因而使用htonl进行转换
  • 另外在多说一句htonl等一系列函数的记忆方法,他的全称就是 Host TO Network Long, 其他的htons ntohl这些就都可以记住了不会搞混
  • 关于addr以及port为什么要转换为network上的binary数据呢, 原因很简单,因为这些数据要在网络上传递,因此我们就需要做这个转换咯

运行性能测评

使用benchmark工具运行代码参数如下

./benchmark -S 127.0.0.1:8080 -cno 1000 -rno 1000

运行结果为

All clients quit, time used is 3m44.353948907s
1000 Clients successfully finished 494000 communicate finally

 

运行在4C 8G 的ArchLinux上 可以看到有很多链接失败,日志里的原因为connection refused,完成1000个client的通信共用3min40秒,因而这种单进程单链接的代码是没有任何实际工程意义的(废话)

 

Leave a Reply

Your email address will not be published. Required fields are marked *

1 × 1 =

This site uses Akismet to reduce spam. Learn how your comment data is processed.