网络编程——基于Linux编写服务器端和客户端

本文介绍如何在Linux环境下实现简单的TCP/IP网络编程,包括服务器端和客户端的搭建过程。通过具体示例,详细讲解了socket函数、bind函数、listen函数、accept函数以及connect函数的应用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考

  1. 《TCP/IP网络编程》 尹圣雨

基于Linux编写服务器端和客户端

编写“Hello World!”服务器端

服务器端是能够受理连接请求的程序。服务器端创建的套接字又称为服务器端套接字或监听(listening)套接字

步骤
  1. 调用socket函数创建套接字

  2. 调用bind函数分配IP地址和端口号

  3. 调用listen函数将套接字转为可接收连接的状态

  4. 调用accept函数受理连接请求

  5. 调用write函数用于传输数据

各函数的原型
  1. scoket函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

成功时返回文件描述符,失败时返回-1

  1. bind函数
#include <sys/socket/h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);

成功时返回0,失败时返回-1

  1. listen函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);

成功时返回0,失败时返回-1

  1. accept函数
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

成功时返回文件描述符,失败时返回-1。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止

代码

该服务器端收到连接请求后向请求者返回“Hello World!”答复。hello_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unisted.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char* argv[])
{
    int serv_sock;
    int clnt_sock;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handling("socket() error");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
    {
        error_handling("bind() error");
    }

    if (listen(serv_sock, 5) == -1)
    {
        error_handling("listen() error");
    }

    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
    {
        error_handling("accept() error");
    }

    write(clnt_sock, message, sizeof(message));
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

编写客户端

客户端套接字的创建过程比创建服务器套接字简单。客户端程序只有“调用socket函数创建套接字”和“调用connect函数向服务器端发送连接请求”这两个步骤。

步骤
  1. 创建套接字,但此时套接字并不马上分为服务器端和客户端。如果紧接着调用bind、listen函数,将成为服务器端套接字;如果调用connect函数,将成为客户端套接字

  2. 调用connect函数向服务器端发送连接请求

connect函数的原型
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);

成功时返回0,失败时返回-1

代码

hello_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, char* argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        error_handling("socket() error");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
    {
        error_handling("connect() error!");
    }

    str_len = read(sock, message, sizeof(message) - 1);
    if (str_len == -1)
    {
        error_handling("read() error!");
    }

    printf("Message from server : %s \n", message);
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

Linux下运行

运行hello_server.c:

gcc hello_server.c -o hserver
./hserver 9190

运行hello_client.c:

gcc hello_client.c -o hclient
./hclient 127.0.0.1 9190

提示:上面的服务器端无法立即重新运行。如果想再次运行,则需要更改之前输入的端口号9190

基于Linux的文件操作

对于Linux而言,socket操作与文件操作没有区别。在Linux世界里,socket也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件I/O的相关函数。Windows是要区分socket和文件的,因此在Windows中需要调用特殊的数据传输相关函数

文件描述符(File Descriptor)

文件描述符是系统分配给文件或套接字的整数。每当生成文件或套接字,操作系统将返回分配给它们的整数,这个整数将成为程序员与操作系统之间良好沟通的渠道。实际上,文件描述符只不过是为了方便称呼操作系统创建的文件或套接字而赋予的数而已。文件和套接字一般经过创建过程才会被分配文件描述符,而下面的3中输入输出对象即使未经过特殊的创建过程,程序开始运行后也会被自动分配文件描述符

  1. 文件描述符0。标准输入:Standard Input

  2. 文件描述符1。标准输出:Standard Output

  3. 文件描述符2。标准错误:Standard Error

文件描述符有时也称为文件句柄,但“句柄”主要是Windows中的术语

打开文件

open函数原型

调用此函数时需传递两个参数:第一个参数是打开的目标文件名及路径信息,第二个参数是文件打开模式(文件特性信息)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int flag);

成功时返回文件描述符,失败时返回-1

文件打开模式
  1. O_CREATE:必要时创建文件

  2. O_TRUNC:删除全部现有数据

  3. O_APPEND:维持现有数据,保存到其后面

  4. O_RDONLY:只读打开

  5. O_WRONLY:只写打开

  6. O_RDWR:读写打开

关闭文件

close函数原型

函数的参数为需要关闭的文件或套接字的文件描述符。此函数不仅可以关闭文件,还可以关闭套接字

#include <unistd.h>

int close(int fd);

成功时返回0,失败时返回-1。fd是需要关闭的文件或套接字的文件描述符

将数据写入文件

write函数原型

函数第一个参数是显示数据传输对象的文件描述符,第二个参数是保存要传输数据的缓冲地址值,第三个参数是要传输数据的字节数

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t nbytes);

成功时返回写入的字节数,失败时返回-1。fd是显示数据传输对象的文件描述符;buf是保存要传输数据的缓冲地址值;nbytes是要传输数据的字节数

此函数定义中,size_t是通过typedef声明的unsigned int类型。对于ssize_t来说,size_t前面多加的s代表signed,即ssize_t是通过typedef声明的signed int类型

例如,通过write函数,创建新文件并保存数据:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unisted.h>
void error_handling(char* message);

int main(void)
{
    int fd;
    char buf[] = "Let's go!\n";

    fd = open("data.txt", O_CREATE|O_WRONLY|O_TRUNC);
    if (fd == -1)
    {
        error_handling("open() error!");
    }
    printf("file descriptor: %d \n", fd);

    if (write(fd, buf, sizeof(buf)) == -1)
    {
        error_handling("write() error!");
    }
    close(fd);
    return 0;
}

void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

读取文件中的数据

read函数原型
#include <unistd.h>

ssize_t read(int fd, void * buf, size_t nbytes);

成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1。fd是显示数据接收对象的文件描述符;buf是要保存接收数据的缓冲地址值;nbytes是要接收数据的最大字节数

例如,通过read函数,读取data.txt中保存的数据

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 100
void error_handling(char* message);

int main(void)
{
    int fd;
    char buf[BUF_SIZE];

    fd = open("data.txt", O_RDONLY);
    if (fd == -1)
    {
        error_handling("open() error!");
    }
    printf("file descriptor: %d \n ", fd);

    if (read(fd, buf, sizeof(buf)) == -1)
    {
        error_handling("read() error!");
    }
    printf("file data: %s", buf);
    close(fd);
    return 0;
}

void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

文件描述符与套接字

同时创建文件和套接字,并比较返回的文件描述符值:

#include <stdio>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>

int main(void)
{
    int fd1, fd2, fd3;
    fd1 = socket(PF_INET, SOCK_STREAM, 0);
    fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC);
    fd3 = socket(PF_INET, SOCK_DGRAM, 0);

    printf("file descriptor 1: %d\n", fd1);
    printf("file descriptor 2: %d\n", fd2);
    printf("file descriptor 3: %d\n", fd3);

    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}

附:以_t为后缀的数据类型

ssize_t、size_t等陌生的数据类型。这些都是元数据类型(primitive),在sys/types.h头文件中一般由typedef声明定义,给熟悉的基本数据类型起了别名

人们目前普遍认为int是32位的,因为主流操作系统和计算机仍采用32位。而在过去16为操作系统时代,int类型是16位的。根据系统的不同、时代的变化,数据类型的表现形式也随之改变,需要修改程序中使用的数据类型。如果之前已在需要声明4字节数据类型之处使用了size_t或ssize_t,则将大大减少代码变动,因为只需要修改并编译size_t和ssize_t的typedef声明即可。在项目中,为了给基本数据类型赋予别名,一般会添加大量typedef声明。而为了与程序员定义的新数据类型加以区分,操作系统定义的数据类型会添加后缀_t

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值