TCP/IP网络编程学习笔记(一)基于TCP/IP的服务器端/客户端通信初探

一、概念

  • 网络编程:编写程序使两台联网的计算机相互交换数据
  • 套接字:网络数据传输所用的软件设备
  • 文件描述符:Linux系统分配给文件或套接字的整数,0-2自动分配给三种标准IO对象(stdin/stdout/stderr),文件和套接字一般要经过创建过程才会分配文件描述符,而3种标准IO对象则无需创建,会被自动分配文件描述符
  • 文件句柄:Windows系统中的文件句柄对应Linux系统中的文件描述符

二、Linux网络编程

1.相关函数

  • socket函数
#include<sys/socket.h>
// 功能:创建套接字
// 参数:
//     domain--协议族
//     type--套接字数据传输类型
//     protocol--通信协议
// 返回值:成功时返回文件描述符,失败时返回 -1
int socket(int domain,int type,int protocol);
  • bind函数
#include<sys/socket.h>
// 功能:给套接字分配IP地址和端口号
// 参数:
//    sockfd--待分配IP地址和端口号的套接字
//    sockaddr--存放IP地址和端口号等信息的结构体
//    addrlen--结构体的长度
// 返回值:成功时返回 0,失败时返回 -1
int bind(int sockfd,struct sockaddr *myaddr,socklen_t addrlen);    
  • listen函数
#include<sys/socket.h>
// 功能:把套接字转化为可接收连接请求状态
// 参数:
//    sock--希望进入等待连接请求状态的(服务器端)套接字的文件描述符
//    backlog--连接请求队列的长度
// 返回值:成功时返回 0,失败时返回 -1
int listen(int sockfd,int backlog);
  • accept函数
#include<sys/socket.h>
// 功能:接受连接请求
// 参数:
//    sock--服务器端套接字的文件描述符
//    addr--保存发起连接请求的客户端地址信息的变量地址值,调用函数后,向被填入客户端地址信息
//    addrlen--存有第二个参数 addr 结构体的长度的变量的地址,调用函数后,该变量即被填入客户端地址长度
// 返回值:成功时返回文件描述符,失败时返回 -1
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
// 调用accept函数时,若等待队列为空,则 accept 函数不会返回,会进入阻塞状态,直到队列中出现新的客户端连接
  • connect函数
#include<sys/socket.h>
// 功能:向服务器端发出连接请求
// 参数:
//    sock  --客户端套接字文件描述符
//    servaddr  --保存目标服务器地址信息的变量的地址值
//    addrlen  --以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
// 返回值:成功时返回 0,失败时返回 -1
int connect(int sockfd,struct sockaddr *serv_addr,socklen_t addrlen);
// 客户端的IP地址和端口号在调用 connect 函数时自动分配,无需调用 bind 函数
// 当服务器端接受连接请求或发生断网等异常情况而中断连接请求时,connect 函数才会返回

3.客户端-服务器端通信程序举例

功能:服务器端收到连接请求后向请求者发送"Hello world!",客户端在控制台输出收到的消息!

  • 服务器端代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>

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

int main(int argc, char* argv[]) {
    int serv_sock;	// 服务器端套接字描述符
    int clnt_sock;	// 客户端套接字描述符
    struct sockaddr_in serv_addr;    // 保存服务器端地址信息
    struct sockaddr_in clnt_addr;    // 保存客户端地址信息

    char message[] = "Hello world!";

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

    // 调用 socket 函数创建套接字
    // 此时的套接字不马上区分服务器端和客户端,若后面调用 bind、listen 函数,则成为服务器端套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error");

    // 将 serv_addr 所指向的内存空间初始化为0
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;	// 指定地址族
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);	// IP地址初始化,利用常数 INADDR_ANY 自动获取服务器端的计算机IP地址
    serv_addr.sin_port = htons(atoi(argv[1]));	// 端口号初始化

    // 调用 bind 函数分配IP地址和端口号
    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");

    // 调用 listen 函数将套接字转为可接收连接状态
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    socklen_t clnt_addr_size = sizeof(clnt_addr);
    // 调用 accept 函数受理连接请求
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
        error_handling("accept() error");

    // 调用 write 函数向客户端传递数据
    write(clnt_sock, message, sizeof(message));

    close(clnt_sock);	// 关闭客户端套接字
    close(serv_sock);	// 关闭服务器端套接字
    return 0;
}
  • 客户端代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>

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

int main(int argc, char* argv[]) {
    int sock;	// 客户端套接字描述符
    struct sockaddr_in serv_addr;
    char message[30];

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

    // 调用 socket 函数创建套接字
    // 此时的套接字并不马上分为服务器端和客户端,若后面调用 connect 函数,则成为客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("scoket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;	// 指定地址族
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);	// IP地址初始化
    serv_addr.sin_port = htons(atoi(argv[2]));	// 端口号初始化

    // 调用 connect 函数向服务器端发出连接请求
    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");

    // 调用 read 函数读取服务器端传输来的数据
    int 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;
}

运行结果:

4.基于Linux的文件操作(Linux中套接字也是看作文件

  • 打开文件
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
// 功能:以指定模式打开指定路径的文件
// 参数:
//    path--文件名的字符串地址
//    flag--文件打开模式信息,组合时用位或运算符“|”
// 返回值:成功时返回文件描述符,失败时返回 -1
int open(const char* path,int flag);

flag的常量值及含义:

O_CREAT  必要时创建文件
O_TRUNC  删除全部现有数据
O_APPEND  维持现有数据,在尾部添加数据
O_RDONLY  只读打开
O_WRONLY  只写打开
O_RDWR  读写打开

  • 关闭文件
#include<unistd.h>
// 功能:借助文件描述符关闭文件
// 参数:
//    fd--需要关闭的文件或套接字的文件描述符
// 返回值:成功时返回 0,失败时返回 -1
int close(int fd);
  • 将数据写入文件
#include<unistd.h>
// 功能:将缓冲区的数据写入到文件中
// 参数:
//    fd--数据传输对象的文件描述符
//    buf--待传输数据的缓冲地址
//    nbytes--传输数据的字节数
// 返回值:成功时返回写入的字节数,失败时返回 -1
ssize_t write(int fd,const void* buf,size_t nbytes);
  • 读取文件中的数据
#include<unistd.h>
// 功能:从文件中读入数据并存入到缓冲区
// 参数:
//    fd--数据接收对象的文件描述符
//    buf--要接收数据的缓冲地址
//    nbytes--要接收数据的最大字节数
// 返回值:成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
ssize_t read(int fd,void* buf,size_t nbytes);
  • 测试代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/socket.h>

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

int main(int argc, char* argv[]) {
    // 测试文件描述符分配
    {
        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);    // 3
        printf("file descriptor 2: %d\n", fd2);    // 4
        printf("file descriptor 3: %d\n", fd3);    // 5

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

    // 测试文件IO
    {
        int fd;
        char buf[] = "Hello world";
        char file_content[20];

        fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC);
        if (fd == -1)
            error_handling("open() error");

        if (write(fd, buf, sizeof(buf)) == -1)
            error_handling("write() error");

        close(fd);

        fd = open("test.txt", O_RDONLY);
        if (fd == -1)
            error_handling("open() error");

        if (read(fd, file_content, sizeof(file_content)) == -1)
            error_handling("read() error");

        printf("file content: %s \n", file_content);

        close(fd);
    }
    return 0;
}

运行结果:

三、Windows网络编程

1.相关函数

  • WSAStartup函数
#include<winsock2.h>
// 功能:设置程序中用到的Winsock版本,并初始化相应版本的库
// 参数:
//    wVersionRequested--要用的 Winsock 版本信息
//    lpWSAData--WSADATA结构体变量的地址值
// 返回值:成功时返回 0,失败时返回非零的错误代码值
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);

借助MAKEWORD宏构建WORD型版本信息
MAKEWORD(1,2);    // 主版本号为1,副版本号为2,返回0x0201
MAKEWORD(2,2);    // 主版本号为2,副版本号为2,返回0x0202

  • WSACleanup函数
#include<winsock2.h>
// 功能:注销Winsock库
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int WSACleanup();
  • socket函数
#include<winsock2.h>
// 功能:创建套接字
// 返回值:成功时返回套接字句柄,失败时返回 INVALID_SOCKET
SOCKET socket(int af,int type,int protocol);
  • bind函数
#include<winsock2.h>
// 功能:给套接字分配IP地址和端口号
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int bind(SOCKET s,const struct sockaddr* name,int namelen);
  • listen函数
#include<winsock2.h>
// 功能:把套接字转化为可接收连接请求状态
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int listen(SOCKET s,int backlog);
  • accept函数
#include<winsock2.h>
// 功能:接受连接请求
// 参数:成功时返回套接字句柄,失败时返回 INVALID_SOCKET
SOCKET accept(SOCKET S,struct sockaddr* addr,int *addrlen);
  • connect函数
#include<winsock2.h>
// 功能:向服务器端发出连接请求
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int connect(SOCKET s,const struct sockaddr* name,int namelen);
  • closesocket函数
#include<winsock2.h>
// 功能:关闭套接字
// 返回值:成功时返回 0,失败时返回 SOCKET_ERROR
int closesocket(SOCKET s);

2.visual studio链接ws2_32.lib

  • 快捷键 Alt+F7-》配置属性-》连接器-》输入-》附加依赖项-》下拉菜单点击编辑-》空白处输入ws2_32.lib-》确定!两个项目都要设置
  • 另一种做法是在文件头加入代码:#pragma comment(lib,"ws2_32.lib"),这是告诉编译器在编译形成的.obj文件和.exe文件中加一条信息,使得链接器在链接库的时候要去找的 wsock32.lib 这个库,把它加入工程中,而不是动态载入 ws2_32.dll。

3.客户端-服务器端通信程序举例

功能:服务器端向连接请求者发送"Hello world!",客户端在控制台输出收到的消息!

  • 服务器端项目代码
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>

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

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hServSock, hClntSock;
    SOCKADDR_IN servAddr, clntAddr;
    
    char message[] = "Hello world!";

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

    // 调用 WSAStartup 函数初始化 Winsock 库
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  
        ErrorHandling("WSAStartup() error");

    // 调用 socket 函数创建服务器端套接字
    hServSock = socket(PF_INET, SOCK_STREAM, 0);    
    if (hServSock == INVALID_SOCKET)
        ErrorHandling("socket() error");

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

    // 调用 bind 函数分配IP地址和端口
    if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)    
        ErrorHandling("bind() error");

    // 调用 listen 函数将套接字转为可接受连接状态
    if (listen(hServSock, 5) == SOCKET_ERROR)
        ErrorHandling("listen() error");

    int szClntAddr = sizeof(clntAddr);
    // 调用 accept 函数受理客户端连接请求
    hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
    if (hClntSock == INVALID_SOCKET)
        ErrorHandling("listen() error");

    // 调用 send 函数向客户端发送数据
    send(hClntSock, message, sizeof(message), 0);
    
    closesocket(hClntSock);
    closesocket(hServSock);
    WSACleanup();
    return 0;
}
  • 客户端项目代码
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
#pragma warning(disable:4996)

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

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    SOCKADDR_IN servAddr;
    char message[30];

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

    // 调用 WSAStartup 函数初始化 Winsock 库
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error");

    // 调用 socket 函数创建客户端套接字
    hSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
        ErrorHandling("socket() error");

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

    // 调用 connect 函数向服务器发出连接请求
    if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
        ErrorHandling("connect() error");

    // 调用 recv 函数读取服务器发送来的数据
    int strlen = recv(hSocket, message, sizeof(message) - 1, 0);
    if (strlen == -1)
        ErrorHandling("read() error");
    printf("Message from server: %s\n", message);

    closesocket(hSocket);
    WSACleanup();
    return 0;
}

运行两个项目的方法:

  • 右击解决方案-》属性-》选择多个启动项目-》下拉选择启动

  • 设置main函数参数

  • Ctrl+F5运行,运行结果:

  • 另一个运行方法:分别运行两个项目,然后找到两个项目的可执行文件,分别在两个cmd窗口运行

4.基于Windows的I/O函数(Windows区分文件和套接字

  • send函数
#include<winsock2.h>
// 功能:向指定套接字写入缓冲区的数据
// 返回值:成功时返回传输字节数,失败时返回 SOCKET_ERROR
int send(SOCKET s,const char* buf,int len,int flags);
  • recv函数
#include<winsock2.h>
// 功能:从套接字读入数据并保存到缓冲区中
// 返回值:成功时返回接收到的字节数(收到EOF时为0),失败时返回 SOCKET_ERROR
int recv(SOCKET s,const char* buf,int len,int flags);

参考书籍:《TCP/IP网络编程》尹圣雨 著,金果哲 译

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值