网络编程套接字(5)——单进程版本的简单TCP网络程序

下面我们来实现一个简单的阻塞式的网络聊天工具:

  • TCP服务器
server.c的作用为接受client的请求,并与client进行简单的数据通信。
#include <stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<stdlib.h>

#define MAX 128

int Startup(char* ip,int port){
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0){
        printf("socket error!\n");
        exit(2);
    }
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = inet_addr(ip);
    local.sin_port = htons(port);

    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
        printf("bind error!\n");
        exit(3);
    }
    
    if(listen(sock,5) < 0){
        printf("listen error!\n");
        exit(4);
    }

    return sock;
}

void service(int sock,char* ip,int port){
    char buf[MAX];
    while(1){
        buf[0] = 0;
        ssize_t s = read(sock,buf,sizeof(buf)-1);
        if(s > 0){
            buf[s] = 0;
            printf("[%s:%d] say# %s\n",ip,port,buf);
            write(sock,buf,strlen(buf));
        }
        else if(s == 0){
            printf("client [%s:%d] quit!\n",ip,port);
            break;
        }
        else{
            printf("read error!\n");
            break;
        }
    }
}

int main(int argc,char* argv[]){
    if(argc != 3){
        printf("Usage:%s [ip] [port]\n",argv[0]);
        return 1;
    }
    int listen_sock = Startup(argv[1],atoi(argv[2]));
    
    struct sockaddr_in peer;
    char ipBuf[24];
    for(;;){
        ipBuf[0] = 0;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
        if(new_sock < 0){
            printf("accept error!\n");
            continue;
        }
        inet_ntop(AF_INET,(const void*)&peer.sin_addr,ipBuf,sizeof(ipBuf));
        int p = ntohs(peer.sin_port);
        printf("get a new connect,[%s:%d]\n",ipBuf,p);

        service(new_sock,ipBuf,p);
        close(new_sock);
    }
    return 0;
}
  • TCP客户端

client.c的作用是连接server,并向server发起通信请求。

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

#define MAX 128

int main(int argc,char* argv[]){
    if(argc != 3){
        printf("Usage:%s [ip] [port]\n",argv[0]);
        return 1;
    }
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0){
        printf("socket error!\n");
        return 2;
    }

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){
        printf("connect error!\n");
        return 3;
    }

    char buf[MAX];
    while(1){
        printf("please Enter# ");
        fflush(stdout);
        ssize_t s = read(0,buf,sizeof(buf)-1);
        if(s > 0){
        buf[s-1] = 0;
        if(strcmp("quit",buf) == 0){
            printf("client quit!\n");
            break;
        }
        write(sock,buf,strlen(buf));
        s = read(sock,buf,sizeof(buf)-1);
        buf[s] = 0;
        printf("server Echo# %s\n",buf);
        }
    }
    close(sock);
    return 0;
}

结果演示:



  • 常用socket  API

1.socket():


socket()打开一个网络通讯端口,若成功则返回一个文件描述符;

应用程序可以像读写文件一样用read/write在网络上收发数据;

扫描二维码关注公众号,回复: 146992 查看本文章

对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。

2.bind():


服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后便可向服务器发起连接,服务器需调用bind绑定一个固定的网络地址和端口号。

bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。

struct   sockaddr*是一个通用指针类型,myaddr参数实际上可接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen来指定结构体的长度。


在程序中对myaddr参数初始化如下:


(1)将整个结构体清零;

(2)设置地址类型为AF_INET;

(3)网络地址为INADDR_ANY,这个宏表示本地的任意IP地址。因为服务器可能有多个网卡,每个网卡也有可能绑定多个IP地址,这样设置可以在所有的IP地址上进行监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。

(4)端口号为SER_PORT,可自行定义。

3.listen():


listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求则忽略。

backlog为一个等待序列,值的设置不会过大(一般为5)。

listen()成功返回0,失败返回-1。


对于等待序列backlog,我们在这里详细讨论一下。举个例子:

我们外出去吃饭时,很多人都遇到过这样一种情况:当餐厅人满时,服务员会让后来的人排号并在餐厅外的一排长凳上稍作等待。当餐厅里的人吃完饭离开,有空闲的桌子时,服务员会根据顺序请外面等待的人进入餐厅就餐。

这里的例子就和backlog很像。设置等待序列是为了使资源达到最大的利用,而backlog值设置不能过大,是因为如果队列过长,同样也会浪费空间和资源。就比如说,一个一百平的餐厅,设置六十平米为用餐区,其余四十平米为等待用餐区,这种规划显然是不合理的。与其占用四十平米的空间设置凳子供客户等待用餐,不如将这块空间用来就餐,扩大用餐区的面积。这样显然更为合理。

4.accept():


三次握手完成后,服务器调用accept()接受连接;

若服务器调用accept()时还没有客户端的连接请求,则阻塞式等待,直到有客户端连接上来;

addr是一个输出型参数,accept()返回时传出客户端的地址和端口号;

若addr参数传NULL,则表示不关心客户端的地址;

addrlen参数为一个输入输出型参数。当它作为输入型参数时,表示缓冲区addr的长度;当它作为输出型参数时,表示客户端地址结构体的实际长度。

5.connect():


客户端需调用connect()连接服务器;

connect与bind的形式参数一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址;

connect()成功返回0,失败返回-1。

  • 相关知识点补充

1.关于listen()和accept()的过程,我们可以举个例子来辅助理解:

我们在去一些景点的小吃街时,每家店面基本上都会有拉客的人在其店面门前吆喝,以此来吸引顾客。当顾客被吸引到其餐厅时,会有服务员来进行相应的服务,而此时拉客的人会回到店面门口继续吆喝拉客。也就是说,拉客的人负责的是将顾客拉进来,而真正给顾客提供服务的是服务员。

与之对应地,listen()相当于拉客的人,它只负责从底层获取新连接;而accept()就充当了服务员的角色,真正提供通信服务。

2.由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。

注:

    (1)客户端不是不允许调用bind(),而是没有必要。否则若在同一台机器上启动多个客户端,就会出现端口号被占用的情况,从而导致不能正确建立连接;

    (2)服务器也不是必须调用bind(),但若服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

3.当调用read()时,返回值有三种情况:

    (1)返回值=0,表示客户端关闭;

    (2)返回值>0,表示读操作成功;

    (3)返回值<0,表示读取失败。

4.Linux下查看监听状态的命令:netstat  -nltp。以上面这个程序为例:


启动服务器,在另一终端下查看监听状态。


此时就可以看到server程序监听9999端口。







猜你喜欢

转载自blog.csdn.net/cecilia3333/article/details/80213705