网络IO

Socket套接字

一、什么是socket
Socket(套接字)是计算机网络编程中的一种抽象概念,它提供了在网络上进行通信的接口。通过使用 Socket,可以在不同计算机之间建立连接,并进行数据的传输和交换。

Socket 可以用于实现各种网络应用,例如:
1. 客户端-服务器通信:通过 Socket,客户端可以与服务器建立连接并发送请求,服务器接收请求并返回响应。
2. 网络聊天和即时通讯:通过 Socket,可以在多个用户之间实现实时的文字、音频或视频通信。
3. 文件传输:可以使用 Socket 在不同计算机之间传输文件,如上传和下载文件。
4. 远程控制:可通过 Socket 在远程计算机上执行指令或操作。
5. P2P(点对点)通信:Socket 提供了直接的点对点通信接口,允许两个或多个计算机直接进行数据交换。

具体使用 Socket 进行网络编程时,需要考虑协议、端口、IP 地址、数据格式等因素,以确保通信的正确性和有效性。不同编程语言和平台都有相应的 Socket API 和库,可以根据自己的需求选择合适的工具和技术。

 在 C 语言中,socket 是一种特殊的文件描述符,它允许程序通过网络与其他程序通信。socket 编程通常包含以下步骤:

  1. 创建 socket
  2. 绑定 IP 地址和端口(服务器)
  3. 监听和接受连接(服务器)
  4. 连接到服务器(客户端)
  5. 发送和接收数据
  6. 关闭连接

socket(int domain, int type, int protocol):创建 socket,返回 socket 文件描述符。

domain:地址族,如 AF_INET(IPv4)或 AF_INET6(IPv6)。
type:通信类型,如 SOCK_STREAM(面向连接)或 SOCK_DGRAM(面向数据报)。
protocol:通常为 0(自动选择协议)。
bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):将 socket 绑定到指定的 IP 地址和端口。

listen(int sockfd, int backlog):将 socket 设置为监听模式,backlog 指定等待连接的队列长度。

accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen):接受客户端连接,返回新的 socket 文件描述符。

connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen):连接到服务器。

send(int sockfd, const void *buf, size_t len, int flags) 和 recv(int sockfd, void *buf, size_t len, int flags):发送和接收数据。

close(int sockfd):关闭 socket 连接。

pthread.h实现多线程

1.线程创建的目标
pthread_create的主要任务就是在一个进程中创建一个新的线程,新的线程共享地址空间和部分资源。

2.使用clone() 系统调用
在Linux中,线程的创建的核心就是使用clone()。clone()与fork()相似,但是比后者更加的灵活,其可以控制新线程和父进程共享的资源和属性。


线程创建的流程:
线程创建的大体流程如下:

准备线程属性:
pthread_create 首先会根据传入的线程属性(如栈大小、调度策略)准备必要的资源。如果没有指定,使用默认值。
例如,默认情况下,新线程会继承进程的地址空间、文件描述符表、信号处理器等。


设置栈空间:
为新线程分配栈空间,通常通过映射新的内存区域来完成(使用 mmap() 或现有栈空间)。
栈空间是线程私有的,即每个线程都有自己的栈,以避免不同线程之间的数据混淆。


调用 clone():
pthread_create 通过 clone() 系统调用创建新线程。clone() 创建一个新的轻量级进程(LWP),它与调用者共享资源(如地址空间),但有自己的栈和执行上下文。
通过 flags 参数指定哪些资源要共享,常见的标志包括:
CLONE_VM: 共享内存空间。
CLONE_FS: 共享文件系统信息。
CLONE_FILES: 共享文件描述符表。
CLONE_SIGHAND: 共享信号处理。


在新线程中执行函数:
clone() 成功后,新线程开始执行 fn 函数,并将传递的 arg 作为参数。
新线程执行函数 fn,直到它返回或显式调用 pthread_exit。
线程 ID 的管理:
pthread_create 返回时,会将新创建线程的 ID (pthread_t) 传给调用者。这通常是一个整数(或指针),在系统中唯一标识该线程。
内核会跟踪这个线程 ID 以及它的相关信息(如状态、栈指针等)。

代码实现

首先是简单的建立TCP通信:

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

#int main(){
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    /*
    AF_INET:使用IPv4地址族
    SOCK_STREAM:创建TCP套接字
    0:使用默认协议(对于SOCK_STREAM是TCP)
    返回的sockfd是一个套接字描述符,用于后续的网络操作。
    */
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(struct sockaddr_in);//声明serveraddr用于存储地址信息

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2048);
    /*
    设置地址族为IPv4
    设置IP地址为INADDR_ANY ,表示服务器将监听所有可用的网络接口(即0.0.0.0)
    设置服务器端口号为2048
    */
    
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)){
        perror("bind");
        return -1;
    }
    //将套接字与指定的地址和端口绑定,若失败返回-1

    listen(sockfd, 10);
    getchar();
}

编译执行以上代码,我们就在2048端口上建立了一个TCP服务器,但是这个服务器目前只能供客户端连接,不能对客户端的交互做出任何反应。

我们可以添加一个连接后做出反馈的功能:

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

#int main(){
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(struct sockaddr_in);

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2048);

    
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)){
        perror("bind");
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;//声明clientaddr用于存储客户端地址

    socklen_t len = sizeof(clientaddr);

    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    //调用 accept 函数接受客户端连接请求。该函数会阻塞,直到有客户端连接为止。
    printf("sockfd: %d,clientfd:%d\n", sockfd, clientfd);



    getchar();
}

编译运行后,我们再按任意键不会直接退出也不会有任何反应,直到我们的客户端连接之后,终端会打印出如下的数据:

sockfd:3,clienrfd:4

现在,我们让服务器返回客户端发送的信息

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

#int main(){
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(struct sockaddr_in);

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2048);

    
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)){
        perror("bind");
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;

    socklen_t len = sizeof(clientaddr);

    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf(“accept\n);

    char buffer[128] = {0};
    int count = recv(clientfd, buffer, sizeof(buffer), 0);
    /*
    调用 recv 函数从客户端接收数据。
    - clientfd :客户端套接字描述符
    - buffer :存储接收数据的缓冲区
    - sizeof(buffer) :缓冲区大小
    */
    
    send(clientfd, buffer, count, 0);
    /*
    调用 send 函数将接收到的数据发送回客户端(回显功能)。
    参数与 recv 类似,但这里发送的是 count 个字节,即实际接收到的数据量。
    */

    printf("sockfd: %d,clientfd:%d\n,count:%d", sockfd, clientfd,count);

    getchar();
    close(clientfd);
}

现在,服务器会回显客户端发送的数据

c此时只能进行单次回显,所以我们可以使用while循环来不断的接收数据:

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

#int main(){
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(struct sockaddr_in);

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(2048);

    
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)){
        perror("bind");
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;

    socklen_t len = sizeof(clientaddr);

    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf(“accept\n);

    while(1){
        char buffer[128] = {0};
        int count = recv(clientfd, buffer, sizeof(buffer), 0);
        //当recv返回0时,说明对端关闭了连接
        if(count == 0){
            break;
        }
        send(clientfd, buffer, count, 0);
        printf("sockfd: %d,clientfd:%d,count = %d\n", sockfd, clientfd,count);
    }
    getchar();
    close(clientfd);
}

现在服务器可以不断的向发送数据的客户端回显数据了,但是如果当客户端断开连接时,程序会卡住,此时端口占用会显示"CLOSE_WAIT"

tcp        0      0 192.168.146.129:2048    192.168.146.1:50701     CLOSE_WAIT  -                    off (0.00/0/0)

此时如果我们用另一个客户端来连接服务器是可以连接的,但是服务器不会对客户端发送的数据做任何反应。此时我们需要引入线程来建立一个基本的网络IO:

#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>

void *client_thread(void *arg){
    int clientfd = *(int *)arg;
    while(1){
        char buffer[128] = {0};
        int count = recv(clientfd, buffer, sizeof(buffer), 0);
        if(count == 0){
            break;
        }
        send(clientfd, buffer, count, 0);
        printf("clientfd:%d,count = %d,buffer: %s\n", clientfd,count,buffer);
    }
    close(clientfd);
}

//tcp
int main(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));

    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//0.0.0

    serveraddr.sin_port = htons(2048);
    if(-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){
        perror("bind");
        return -1;
    }
    

    listen(sockfd, 10);

    
    printf("accept\n");

    while(1){
        struct sockaddr_in clientaddr;
        socklen_t len = sizeof(clientaddr);
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);

        pthread_t thid;
        pthread_create(&thid, NULL, client_thread, &clientfd);
        
    }
    
    getchar();


}

由此,我们便实现了最基本的网络IO。(未引入IO复用, 每个客户端分配一个线程)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐