网络编程基础-简单的TCP阻塞式网络聊天应用(单进程版本)

11 篇文章 0 订阅

个人博客传送门

本程序使用的TCP协议,该协议是面向连接、通过字节流进行通信的。实现了客户端和服务器端的阻塞式通信。主要锻炼了对于socket API的使用。

程序使用的函数

其中socket、bind、地址转换函数网络基础编程-UDP为例已经分析过。这里介绍的函数适用于TCP这类面向连接的协议。

listen

当我们的网络程序需要使用TCP面向连接一类的协议的时候,socket中选用了SOCK_STREAM选项。此时需要我们的服务器端进入listen监听状态,用来监听客户端的连接。

//让服务器进入listen监听状态
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//成功返回0,失败返回-1

参数

backlog指的是连接队列,将未进入连接的请求处于排序状态。比如现在客户端A已经连接中,此时客户端B想要进行连接的时候,如果服务器端没法提供服务,就将客户端B放入连接等待队列。

该参数不能设置的太大,也不能设置的太小。太大就会浪费服务器端资源。太小就会让处于连接等待的客户端数量减小。如果连接队列已经满了,就会忽略后来的链接请求。

accept

accept函数在服务器端调用了listen函数之后,进行连接等待。

//接受客户端的连接请求。
#include <sys/socket.h>
int accept(int sockfd, struct sockadrr* addr, socklent_t* len);
//成功返回文件描述符,失败返回-1

参数

sockfd是用来socket函数返回的套接字;addr是用来存放连接客户端的地址数据;len存放连接客户端的addr的大小。

注意:

accept函数返回了一个文件描述符,这个文件描述符要和socket返回的文件描述符区分开来。 socket的fd是用来建立连接使用的,之后我们使用了listen函数将它变成了用来监听的套接字;accept返回的是请求连接的客户端的文件描述符,该文件描述符用来进行数据传输。

返回的accept_fd和socket函数的socket_fd有着同样的套接字类型和地址族。如果服务器端并不关心客户端的地址和长度,可以将accept函数的后两个参数设置为NULL。

如果没有连接进入的时候,accept默认会阻塞等待。但是我们可以通过让套接字进入非阻塞状态,这样accept会返回-1,同时将errno设置为EAGAIN。本程序使用阻塞式。

connect
//客户端通过该函数向服务器端发起连接请求。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* addr, socklen_t len);
//成功返回0,失败返回-1

参数

sockfd是socket函数返回的文件描述符;addr是服务器端的地址族;len是addr的大小。

send

该函数跟write基本一样,就是多了一个flags选项。同时send通常用于面向连接的协议,如当前的TCP。用于UDP的是sendto函数,但是TCP也可以使用sendto函数。

#include <sys/types.h>
#include <sys/socket.h>
//用来数据发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//成功返回发送的字节数,失败返回-1

参数

sockfd是写入发送数据的文件描述符;buf是发送数据的缓冲区;len是发送的字节数;flags是标志位,通过他可以实现面向连接协议的一些特定功能,通过我们让flags为0,表示不使用。这样功能和write就是一样的。

flags

注意

就算send函数成功返回,也不代表客户端成功接收,只是说明该数据已经成功进入了当前的网络驱动中。

recv
#include <sys/types.h>
#include <sys/socket.h>
//用来数据接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//成功返回数据的字节数。如果没有可用数据或者对等方已经按序结束,返回0;如果出错返回-1

recv的参数和send一样,只是flags的选项有些不一样。

flags

代码

服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

//定义服务器IP地址
#define SERVICE_IP INADDR_ANY
//定义listen等待队列
#define WAIT_QUEUE 5

// ./service port
int main(int argc, char* argv[]){
    if(argc != 2){
        printf("Usage: %s port\n", argv[0]);
        return 1;
    }
    //创建服务器套接字
    int service_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(service_sock < 0){
        printf("create service_sock failed, errno : %d, error_string: %s\n", errno, strerror(errno));
        return 2;
    }
    //初始化
    struct sockaddr_in service_socket;

    bzero(&service_socket, sizeof(service_socket));
    service_socket.sin_family = AF_INET;
    service_socket.sin_port = htons(atoi(argv[1]));
    service_socket.sin_addr.s_addr = htonl(SERVICE_IP);
    socklen_t service_socket_len = sizeof(service_socket);
    //绑定端口号和IP地址
    if((bind(service_sock, (struct sockaddr*)(&service_socket), service_socket_len)) < 0){
        printf("bind failed, errno : %d, error_string: %s\n", errno, strerror(errno));
        close(service_sock);
        return 3;
    }
    //设置为监听状态
    if((listen(service_sock, WAIT_QUEUE)) < 0){
        printf("listen failed, errno : %d, error_string: %s\n", errno, strerror(errno));
        close(service_sock);
        return 4;
    }

    printf("service_socket bind listen success, wait accept...\n");
    //进入循环等待链接
    struct sockaddr_in client_socket;
    while(1){
        socklen_t client_socket_len = sizeof(client_socket);
        //随时等待接入的客户端
        int client_sock = accept(service_sock, (struct sockaddr*)(&client_socket), &client_socket_len);
        if(client_sock < 0){
            printf("accept failed, errno : %d, error_string: %s\n", errno, strerror(errno));
            printf("wait next accept\n");
            continue;
        }
        //获取接入客户端的IP地址和端口号并输出
        /*INET_ADDRSTRLEN 16*/
        char client_ip_buf[INET_ADDRSTRLEN];
        char* ptr = inet_ntop(AF_INET, &client_socket.sin_addr, client_ip_buf, sizeof(client_ip_buf));
        if(ptr == NULL){
            printf("inet_ntop failed, errno : %d, error_string: %s\n", errno, strerror(errno));
            continue;
        }
        printf("a new accept, client ip: %s, client port: %d\n", client_ip_buf, ntohs(client_socket.sin_port));
        printf("waiting message from client\n");
        //跟当前客户端进行阻塞式数据通信
        while(1){
            char buf[1024];
            bzero(buf, sizeof(buf));
            //等待客户端的数据
            int num = recv(client_sock, buf, sizeof(buf), 0);
            if(num == 0){
                printf("client quit\n");
                close(client_sock);
                break;
            }
            if(num > 0){
                printf("client# %s\n", buf);
                printf("service# ");
                bzero(buf, sizeof(buf));

                fgets(buf, sizeof(buf), stdin);
                buf[strlen(buf)-1] = '\0';
                //发送数据给客户端
                int num1 = send(client_sock, buf, strlen(buf)+1, 0);
                if(num1 < 0){
                    printf("send failed, errno : %d, error_string: %s\n", errno, strerror(errno));
                    break;
                }
            }
            else{
                printf("recv failed, errno : %d, error_string: %s\n", errno, strerror(errno));
                break;
            }
        }
        close(client_sock);
    }
    return 0;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

// ./client service_ip service_port
int main(int argc, char* argv[]){
    if(argc != 3){
        printf("Usage: %s service_ip service_port\n", argv[0]);
        return 1;
    }
    //创建套接字
    int client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if(client_sock < 0){
        printf("create client_sock failed, errno : %d, error_string: %s\n", errno, strerror(errno));
        return 2;
    }
    //初始化套接字
    struct sockaddr_in client_socket;

    bzero(&client_socket, sizeof(client_socket));
    client_socket.sin_family = AF_INET;
    client_socket.sin_port = htons(atoi(argv[2]));
    int num = inet_pton(AF_INET, argv[1], &client_socket.sin_addr);
    if(num <= 0){
        if(num == 0){
            fprintf(stderr, "inet_pton not in presentation format");
        }
        else{
            printf("inet_pton failed, errno : %d, error_string: %s\n", errno, strerror(errno));
        }
        return 3;
    }
    socklen_t client_socket_len= sizeof(client_socket);
    //发起请求连接
    if((connect(client_sock, (struct sockaddr*)(&client_socket), client_socket_len)) < 0){
        printf("connect failed, errno : %d, error_string: %s\n", errno, strerror(errno));
        close(client_sock);
        return 3;
    }
    printf("connect success ...\n");
    printf("please enter message\n");
    //进入连接,开始循环发送数据
    char buf[1024];
    while(1){
        bzero(buf, sizeof(buf));
        printf("client# ");
        fgets(buf, sizeof(buf), stdin);

        buf[strlen(buf)-1] = '\0';
        //发送数据
        int num1 = send(client_sock, buf, strlen(buf)+1, 0);
        if(num1 > 0){
            bzero(buf, sizeof(buf));
            int num2 = recv(client_sock, buf, sizeof(buf), 0);
            if(num2 < 0){
                //阻塞式等待数据接收
                printf("recv failed, errno : %d, error_string: %s\n", errno, strerror(errno));
                break;
            }
            printf("service# %s\n", buf);
        }
        else{
            printf("send failed, errno : %d, error_string: %s\n", errno, strerror(errno));
            break;
        }
    }
    close(client_sock);
    printf("client quit!!\n");
    return 0;
}
Makefile
.phony:all
all:service client

flags=-Wall

service:service.c
    gcc -o $@ $^ $(flags)
client:client.c
    gcc -o $@ $^ $(flags)

.phony:clean
clean:
    rm -f service client
实验截图:

这里是同一台虚拟机的不同终端下的测试,其实如果连接的是同一个网段,用不同的电脑或者用手机也是可以连接的,但是由于手机没有客户端代码,只能获取到信息,并不能进行通信。

tcp截图

手机通过浏览器,使用http协议进行访问,就相当于直接输入了http报头的信息。

单线程版本的缺点

单进程最大的问题不是效率,是每次服务器只能够处理一个客户端,这样是很不正常的。正常的是我们的服务器可以跟多个客户端一起连接,然后可以同时跟每个客户端一起通信。

单进程版本只能等一个client结束之后再处理下一client的请求。我们可以通过多线程和多进程的版本来解决这个问题。

参考资料

  • UNIX环境高级编程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值