socket-tcp两机通讯实现-C语言

1.简介

原本是想使用C语言搭建一个简易的HTTP服务器,但发现HTTP服务器搭建与采用套接字实现TCP通信之间就只差了规定的HTTP报文而已。因此用这篇博客梳理一下socket-tcp的编程过程:server/client模式,客户端向服务端发送一个字符串,服务端向客户端返回字符串的大写形式。
硬件环境:

  • VMware虚拟机运行ubuntu20.04作为服务端,本机windows 10系统作为客户端;
  • VMware虚拟机网络采用桥接模式,通讯端口设置为8080,记得打开防火墙端口:sudo ufw allow 8080
  • 可以在通讯前ping一下,看连接是否成功。

两端的通信流程:
TCP通信流程

2.服务端socket-tcp-server

2.1 创建套接字

int serverfd = socket(AF_INET, SOCK_STREAM, 0) //创建服务器端套接字文件

在Linux中,所有硬件都已文件的形式存在,int serverfd 就代表服务器端的套接字。函数原型为:

int socket(int domain, int type, int protocol);

int domain:选择通信时的协议族,常用设置为AF_INET(因特网,使用IPv4格式的IP地址)和AF_UNIX(本地进程)。
int type:socket的连接类型。常见的SOCK_STREAM——TCP协议;SOCK_DGRAM——UDP协议。
int protocol:0代表默认协议。

2.2 绑定套接字

将套接字绑定至某一个特定的进程(即我们的服务端)。

bind(serverfd, (struct sockaddr *)&servaddr, sizeof(servaddr))

函数原型:

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

int sockfd:套接字的文件描述符,及我们采用socket函数创建的返回值。
const struct sockaddr *addr:描述服务器端口地址的结构体。
socklen_t addrlen:第二个参数addr的长度。

struct sockaddr这个东西定义有好几种形式(有点像C++中继承的关系):IPv4地址用结构体sockaddr_in表示,IPv6地址用结构体sockaddr_in6,这两个的地址格式定义在库文件netinet/in.h中。我们根据不同的情况创建 sockaddr_in 或者 sockaddr_in6,给bind函数传参时进行一下类型转换就OK了。也正是因为这个原因,才会有第三个参数输入结构体长度 addrlen 的必要。实现如下:

struct sockaddr_in servaddr
socklen_t cliaddr_len;

bzero(&servaddr, sizeof(servaddr));           // 结构体清零
servaddr.sin_family = AF_INET;                // 设置地址类型为AF_INET         
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置网络地址为INADDR_ANY
servaddr.sin_port = htons(LISTENPORT);        // 设置端口号

bind(serverfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

INADDR_ANY表示服务器可以接受来自任意网络接口的连接请求;
htonl函数:字节序转换函数,位于库arpa/inet.h
TCP/IP协议规定,网络数据流应采用大端模式存储。主机的存储可能是大端也可能是小端。因此需要将用于将主机字节序(Host Byte Order)转换为网络字节序(Network Byte Order),以确保在不同字节序的系统上都能正常工作。函数原型如下:

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);

2.3 监听并设置最大连接数

listen(serverfd, MAXLISTENNUM);

使已绑定的socket监听对应客户端进程状态,并同时设置服务器同时可建立的连接的数量。

至此,服务器的服务已经开启,只等待客户端发起连接。

2.4 监听并处理客户端信息

若要实现一直监听客户端请求,以下步骤放进while循环中。

clientfd = accept(serverfd, (struct sockaddr *)&clientaddr, &cliaddr_len);
ssize_t byteRead = recv(clientfd, buffer, BUFFSIZE, 0);
ssize_t byteSend = send(clientfd, buffer, byteRead, 0);
close(clientfd);
  1. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    阻塞等待客户端的连接请求。如若没有客户端请求,程序会在此处阻塞等待。struct sockaddr *addr是传出参数(待被填充的数组),用于存储客户端的信息。参数addrlen是一个传入传出参数,传入时为函数调用者提供的缓冲区addr的长度,传出时为客户端地址结构体的实际长度。
  2. ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    用于从已连接的套接字中接收信息,返回值是接收信息的长度。int sockfd代表从哪个文件描述符获得数据,即由accept获得的客户端套接字。void *bufsize_t len分别指接收信息存放的数组及其大小。int flags表示调用的执行方式(阻塞/非阻塞)。
  3. ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    向处于连接状态的套接字中发送数据,返回值是发送信息的长度。与recv函数类似,void *bufsize_t len分别指发送信息存放的数组及其大小。
  4. int close(int fd);
    释放系统分配给套接字的资源。关闭客户端的连接。

补充:

  • 如果需要打印客户端的IP和端口,需使用inet_ntoa(clientaddr.sin_addr)ntohs(clientaddr.sin_port),用于将IP地址格式转换为二进制格式。
  • 上述用于socket的大多主要函数返回值为-1时表示运行时出错。

2.5 服务端源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <ctype.h>

#define LISTENPORT 8080
#define MAXLISTENNUM 10
#define BUFFSIZE 1024

int main()
{
    struct sockaddr_in servaddr,clientaddr; //Ipv4
    socklen_t cliaddr_len;
    int serverfd;
    int clientfd;
    char buffer[BUFFSIZE];

    /*1.创建服务器端套接字文件*/
    if((serverfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        fprintf(stderr,"SERVER:create server socket error");
        return -1; 
    }

    /*2.初始化服务器端口地址*/
    bzero(&servaddr, sizeof(servaddr));           // 结构体清零
    servaddr.sin_family = AF_INET;                // 设置地址类型为AF_INET         
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置网络地址为INADDR_ANY
    servaddr.sin_port = htons(LISTENPORT);        // 设置端口号

    /*3.将套接字文件与服务器端口地址绑定*/
    if(bind(serverfd, (struct sockaddr *)&servaddr, sizeof(servaddr))==-1){
        fprintf(stderr,"SERVER:bind server socket error");
        return -1;
    }

    /*4.监听并设置最大连接数*/
    if(listen(serverfd, MAXLISTENNUM)==-1){
        fprintf(stderr,"SERVER:listen on socket error");
        return -1;
    }

    printf("SERVER:Server started. Listening on port %d...\n", LISTENPORT);

    
    while (1)
    {
        cliaddr_len = sizeof(clientaddr);
        if((clientfd = accept(serverfd, (struct sockaddr *)&clientaddr, &cliaddr_len)) == -1){
            fprintf(stderr,"SERVER:accept on socket error");
            return -1;
        }

        ssize_t byteRead = recv(clientfd, buffer, BUFFSIZE, 0);
        if(byteRead == -1){
            fprintf(stderr,"SERVER:receive socket content error");
            return -1;
        }
        printf("SERVER:received from %s at PORT %d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));

        for(int i=0;i<byteRead;i++){
            buffer[i] = toupper(buffer[i]);
        }

        ssize_t byteSend = send(clientfd, buffer, byteRead, 0);
        if(byteSend == -1){
            fprintf(stderr,"SERVER:send content to socket error");
            return -1;
        }
        close(clientfd);
    }
    return 0;
}

3. 客户端socket-tcp-client (windows版)

客户端和服务端大多数套接字使用的接口相同。

3.1 windows中的socket

在windows中,不采用<sys/socket.h>库。而是采用:

#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

并且在程序启动最开始运行以初始化Winsock库。

WORD sockVersion = MAKEWORD(2, 2);
WSADATA data;
if (WSAStartup(sockVersion, &data) != 0) // Windows异步套接字的启动命令
{
	fprintf(stderr, "CLIENT:Failed to initialize Winsock Error.\n");
	return -1;
}

3.2 struct sockaddr_in初始化

struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8080);
serAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.89");

需要设置服务器的监听端口号和服务器的IP地址。

3.3 客户端windows源码

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
#include <STDIO.H>

#define LISTENPORT 8080
#define BUFFSIZE 1024

int main(int argc, char *argv[])
{
    char recData[BUFFSIZE];
    /*1.初始化Winsock库*/
    WORD sockVersion = MAKEWORD(2, 2);
    WSADATA data;
    if (WSAStartup(sockVersion, &data) != 0) // Windows异步套接字的启动命令
    {
        fprintf(stderr, "CLIENT:Failed to initialize Winsock Error.\n");
        return -1;
    }

    if (argc != 2)
    {
        fprintf(stderr, "CLIENT:Number of Program Input Error\n");
        return -1;
    }
    char *str = argv[1];

    /*2.创建套接字 */
    SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sclient == INVALID_SOCKET)
    {
        fprintf(stderr, "CLIENT:Create Socket Error.\n");
        return 0;
    }

    /*3.初始化服务器端口地址*/
    struct sockaddr_in serAddr;
    serAddr.sin_family = AF_INET;
    serAddr.sin_port = htons(8080);
    serAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.89");

    /*4.请求链接*/
    if (connect(sclient, (struct sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) // 连接
    {
        fprintf(stderr, "CLIENT:Connect Server Error\n");
        closesocket(sclient);
        return 0;
    }

    /*5.发送数据*/
    ssize_t byteSend = send(sclient, str, strlen(str), 0);

    /*6.接收客户端返回的数据*/
    ssize_t byteRead = recv(sclient, recData, BUFFSIZE , 0);
    printf("Response from server:%s\n", recData);

    /*7.关闭连接*/
    closesocket(sclient);

    WSACleanup(); // 功能是终止Winsock 2 DLL (Ws2_32.dll) 的使用
    return 0;
}

编译的时候记得加上 -lwsock32

4. 客户端socket-tcp-client (ubuntu版)

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

#define SERVEPORT 8080
#define BUFFSIZE 1024


int main(int argc, char *argv[]){   //其中argv[0]指向程序名称本身,argv[1]指向第一个命令行参数
    int clientfd;
    struct sockaddr_in servaddr;
    socklen_t addrlen;
    
    char buffer[BUFFSIZE];

    if(argc != 2){
        fprintf(stderr,"CLIENT:number of program input error");
        return -1;
    }
    char* str = argv[1];

    /*1.创建服务器端套接字文件*/
    if((clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        fprintf(stderr,"CLIENT:create client socket error");
        return -1;
    }

    /*2.初始化服务器端口地址*/
    bzero(&servaddr, sizeof(servaddr));           // 结构体清零
    servaddr.sin_family = AF_INET;                // 设置地址类型为AF_INET    
    inet_pton(AF_INET, "192.168.1.89", &servaddr.sin_addr);   //设置目的IP
    servaddr.sin_port = htons(SERVEPORT);        // 设置目的端口号

    /*3.请求链接*/
    if(connect(clientfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) == -1){
        fprintf(stderr,"CLIENT:connect server error");
        return -1;
    }
    
    /*4.发送数据*/
    ssize_t byteSend = send(clientfd, str, strlen(str), 0);

    /*5.接收客户端返回的数据*/
    ssize_t byteRead = recv(clientfd, buffer, BUFFSIZE, 0);
    printf("Response from server:%s\n",buffer);

    /*6.关闭连接*/
    close(clientfd);

    return 0;
}

可参考的链接:
https://book.itheima.net/course/223/1277519158031949826/1277528764959432706

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值