笔记 C socket 实现简单的套接字编程(客户端和客户端连接)

0. 套接字

网上很多说套接字名称包括:
IP:port
  • 在 《Windows Socket 网络编程》 Bob Quinn 著的一书中,套接字名称包括:
1. IP
2. port
3. protocol (协议)
  • 服务端创建套接字 (IP, port, protocol) 和客户端的套接字 (IP, port, protocol) 可以通过套接字编程连接起来。WindowsWinSOCKET 编程,Linuxlinux socket 编程,两者不兼容

环境:

VS 2019

1. 图示流程(图网上找的,代码自己改的)

在这里插入图片描述

  • 说明:刚学习 windows socket 编程,能搞懂图中的函数怎么用及其参数的意义算是入门了。

2. 库 <winsock2.h>

#include <iostream>
#include <winsock2.h>
#include <string>

// 使用 <winsock2.h>,需要链接的动态链接库 "Ws2_32.lib"
#pragma comment(lib, "Ws2_32.lib")

  • 注: 不使用旧版本的 <winsock.h>,使用较新的 <winsock2.h>

3. 初始化动态链接库

	// 初始化 DLL
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsdata;
	if (WSAStartup(sockVersion, &wsdata) != 0) {
		return EXIT_FAILURE;
	}
解释
  • WORD: 一个宏定义,一个 无符号短整型
typedef unsigned short      WORD;

  • MAKEWORD: 一个宏定义,获取 Windows Sockets 规范的版本(2, 2) 说明获取 2.2 版本,到 2021-01-10 最高为 2.2 版本
#define MAKEWORD(a, b)      ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))

  • WSADATA : 一个结构体,存放 windows socket 初始化信息,具体如下(看注释):
typedef struct WSAData {
        WORD                    wVersion;									// 将使用的 Winsock 版本号
        WORD                    wHighVersion;								// 载入的 Winsock 动态库支持的最高版本,高字节代表次版本,低字节代表主版本
#ifdef _WIN64																// 如果是 64 位
        unsigned short          iMaxSockets;								// 最大数量的并发 Sockets,其值依赖于可使用的硬件资源
        unsigned short          iMaxUdpDg;									// 数据报的最大长度
        char FAR *              lpVendorInfo;								// 为 Winsock 实现而保留的制造商信息
        char                    szDescription[WSADESCRIPTION_LEN+1];		// 由特定版本的 Winsock 设置,实际上没有太大用处。
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];		// 同上
#else
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
        unsigned short          iMaxSockets;
        unsigned short          iMaxUdpDg;
        char FAR *              lpVendorInfo;
#endif
} WSADATA, FAR * LPWSADATA;

注释参考: https://blog.csdn.net/huachizi/article/details/89401476


  • WSAStartup(sockVersion, &wsdata)重点,通过进程启动 Winsock DLL 的使用。
int WSAAPI WSAStartup(
    _In_ WORD wVersionRequested,
    _Out_ LPWSADATA lpWSAData
    );

如果成功,WSAStartup 函数将返回 0。否则,它将返回下面列出的错误代码之一。
在这里插入图片描述


4. 正文:进入 1. 图示流程 中流程图的操作

4.1 服务端,按流程图实现

在这里插入图片描述

  • 代码
#include <iostream>
#include <winsock2.h>
#include <string>

#pragma comment(lib, "Ws2_32.lib")



using namespace std;


int main() {

	// 初始化 DLL
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsdata;
	if (WSAStartup(sockVersion, &wsdata) != 0) {
		return EXIT_FAILURE;
	}


	// 创建套接字
	SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (serverSocket == INVALID_SOCKET) {
		cout << "create socket failure!!" << endl;
		return EXIT_FAILURE;
	}


	// 绑定套接字
	sockaddr_in socketAddr;
	socketAddr.sin_family = AF_INET;
	socketAddr.sin_port = htons(8080);
	socketAddr.sin_addr.S_un.S_addr = INADDR_ANY;

	if (bind(serverSocket, (sockaddr*)&socketAddr, sizeof(socketAddr)) == SOCKET_ERROR) {
		cout << "bind error!" << endl;
		return EXIT_FAILURE;
	}


	// 监听
	if (listen(serverSocket, 10) == SOCKET_ERROR) {
		cout << "listen error!!" << endl;
	}


	// 接收客户端套接字
	SOCKET clientSocket;
	sockaddr_in client_sin;
	// store message
	int len = sizeof(client_sin);

	cout << "waitting to connect......" << endl;
	clientSocket = accept(serverSocket, (sockaddr*)&client_sin, &len);
	if (clientSocket == INVALID_SOCKET) {
		cout << "accept error" << endl;
		return EXIT_FAILURE;
	}
	else {
		cout << "get a connection: " << inet_ntoa(client_sin.sin_addr) << endl;
	}


	// 看流程图,循环
	while (true) {


		// 接收消息
		const size_t messageLen = 1000;
		char recClientMsg[messageLen];
		int num = recv(clientSocket, recClientMsg, messageLen, 0);
		// 截断数组
		recClientMsg[num] = '\0';
		if (num > 0) {
			cout << "Client say: " << recClientMsg << endl;
		}

		// 退出
		if (strcmp(recClientMsg, "exit()") == 0) {
			break;
		}

		// 发送数据
		cout << "Input message you want to send to client:" << endl;
		string data;
		getline(cin, data);
		const char* sendClientMsg;
		sendClientMsg = data.c_str();
		send(clientSocket, sendClientMsg, data.size(), 0);

		// 退出
		if (strcmp(sendClientMsg, "exit()") == 0) {
			break;
		}

	}

	closesocket(clientSocket);
	closesocket(serverSocket);
	WSACleanup();


	return EXIT_SUCCESS;
}

4.2 客户端,按流程图实现

在这里插入图片描述

  • 代码
#include <winsock2.h>
#include <iostream>
#include <string>



using namespace std;

#pragma comment(lib, "ws2_32.lib")


int main() {

    // 初始化
    WORD sockVersion = MAKEWORD(2, 2);
    WSADATA data;
    if (WSAStartup(sockVersion, &data) != 0) {
        return EXIT_SUCCESS;
    }


    // 创建客户端套接字
    SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket == INVALID_SOCKET) {
        cout << "Socket error" << endl;
        return EXIT_FAILURE;
    }


    // 连接服务端
    sockaddr_in sock_in;
    sock_in.sin_family = AF_INET;
    sock_in.sin_port = htons(8080);
    sock_in.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    if (connect(clientSocket, (sockaddr*)&sock_in, sizeof(sock_in)) == SOCKET_ERROR) {
        cout << "Connect error" << endl;
        return EXIT_FAILURE;
    }
    else {
        cout << "Connect successfully." << endl;
    }


    // 看流程图,循环
    while (true) {
        
        // 发送信息
        cout << "Input message you want send:" << endl;
        string data;
        getline(cin, data);
        const char* sendServerMsg;
        sendServerMsg = data.c_str();
        send(clientSocket, sendServerMsg, strlen(sendServerMsg), 0);
        
        // 判断结束循环
        // 退出
        if (data == "exit()") {
            break;
        }


        // 接收信息
        const size_t messageLen = 1000;
        char revServerMsg[messageLen];
        int num = recv(clientSocket, revServerMsg, messageLen, 0);
        // 截断数组
        revServerMsg[num] = '\0';
        if (num > 0) {
            cout << "Sever say:" << revServerMsg << endl;
        }


        // 判断结束循环
        // 退出
        if (strcmp(revServerMsg, "exit()") == 0) {
            break;
        }
    }

    // 关闭客户端套接字
    closesocket(clientSocket);
    WSACleanup();

	return EXIT_SUCCESS;
}


4.3 启动

注意记得修改按 Alt + P + PAlt 按住, P 按两下):

在这里插入图片描述

1. 启动服务端
  • 服务端:
    在这里插入图片描述
2. 再启动客户端
  • 客户端:
    在这里插入图片描述
  • 服务端:
    在这里插入图片描述

4.4 问题

  • 简单实现果然还有还多问题,勉强能进行 一问一答 式通信

5. Linux socket

  • server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 4096
#define PORT 6666

int main(int argc, char **argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buff[MAXLINE];
    int n;

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    printf("======waiting for client's request======\n");
    while (1)
    {
        if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) == -1)
        {
            printf("accept socket error: %s(errno: %d)", strerror(errno), errno);
            continue;
        }
        n = recv(connfd, buff, MAXLINE, 0);
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff);
        close(connfd);
    }

    close(listenfd);
}
  • client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 4096
#define PORT 6666

int main(int argc, char **argv)
{
    int sockfd, n;
    char recvline[MAXLINE], sendline[MAXLINE];
    struct sockaddr_in servaddr;

    if (argc != 2)
    {
        printf("usage: ./client <ipaddress>\n");
        exit(0);
    }

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        printf("inet_pton error for %s\n", argv[1]);
        exit(0);
    }

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    printf("send msg to server: \n");
    fgets(sendline, MAXLINE, stdin);
    if (send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
        printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    close(sockfd);
    exit(0);
}

5.1 服务端:

g++ -o server server.c
# 生成 server 执行文件

./server
  • 查看端口 6666 占用
[chen@hecs-70768 socket]$ lsof -i:6666
COMMAND   PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
server  11795 chen    3u  IPv4 4725561      0t0  TCP *:ircu-2 (LISTEN)

5.2 客户端

g++ -o client client.c
# 生成 client 执行文件

# xxx.xxx.xxx.xxx 是服务端的 IP,后面会提示你输入文字,在这里先不输入:
zhiyong@LAPTOP-OC4RD91F:/mnt/d/Computer/network/socket$ ./client xxx.xxx.xxx.xxx
send msg to server:

5.3 在服务端的机器上面查看端口占用:

会显示服务端的端口显示ESTABLISHED

[chen@hecs-70768 socket]$ lsof -i:6666
COMMAND   PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
server  11795 chen    3u  IPv4 4725561      0t0  TCP *:ircu-2 (LISTEN)
server  11795 chen    4u  IPv4 4727103      0t0  TCP hecs-70768:ircu-2->117.61.106.77:36486 (ESTABLISHED)

5.4 抓包

在这里插入图片描述
客户端执行 ./client xxx.xxx.xxx.xxx 的时候,首先执行 connect ,在内核中就是执行 TCP 三次握手。
客户端发起 connect,服务端一直处于 listen 状态,然后在 accept,在接收到客户端的 connect 后,返回一个客户端的套接字。然后这个客户端发送的数据,就是通过这个套接字去读取。读取的方法跟打开文件的 IO 一样。

5.5 客户端在 close 后,会进入 TIME_WAIT 模式

  • 可以先在服务端使用 lsof -i:6666 查看客户端的端口,也可以通过抓三次握手的报文来获取端口。客户端发起连接的端口为 59438,使用命令netstat -a | grep 59438 查看,如下:
tcp        0      0 hecs-70768:ircu-2       hecs-70768:59438        TIME_WAIT

5.6 一台服务器能监听多少个客户端?

服务端中的 accept 函数返回一个客户端套接字 clientFd,这个跟客户端的IP、端口相关。
理论上世界上所有的 IP 都可以作为客户端,每个客户端有 65536 个端口,而全世界的 IP 约有 四十亿个地址,所以有的文章说一台服务器最多有 四十亿个✖65546。但是一台服务器需要一个监听套接字 serverFd,当有一个客户端连接进来的时候,需要使用 accept 接收并建立一个套接字 clientFd,而 accept 返回值是 int 类型,一般都大于 0 。而除去标准输入输出的 0, 1, 2 和自身的监听套接字serverFd外,所以理论上最多应该是建立 int 最大值 - 4 的连接数。同时, 因为每条连接占内存,实际上一般就一百多万条就搞不住了。

5.7 TCP 四次握手

  1. 无论是服务端还是客户端,都可以主动的发起close 操作,只要有一方发起,就会进行四次握手,这里说的发起连接是指:客户端就不用说了,就是指自身的套接字。服务端指的是accept 得到的客户端套接字clientFd,而不是服务端的套接字serverFd。如果关闭了服务端的套接字serverFd,相当于服务端挂掉了。
    下面是一个例子:
  • 服务端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 4096
#define PORT 6666

int main(int argc, char **argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buff[MAXLINE];
    int n;

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);

    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    printf("======waiting for client's request======\n");
    // while (1)
    {
        if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL)) == -1)
        {
            printf("accept socket error: %s(errno: %d)", strerror(errno), errno);
            // continue;
        }
        n = recv(connfd, buff, MAXLINE, 0);
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff);
        close(connfd);  // 服务端先发起 close
    }

    close(listenfd);  // 马上跟着就关闭了服务端的套接字,相当于服务端挂机了
}
  • 客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 4096
#define PORT 6666

int main(int argc, char **argv)
{
    int sockfd, n;
    char recvline[MAXLINE], sendline[MAXLINE];
    struct sockaddr_in servaddr;

    if (argc != 2)
    {
        printf("usage: ./client <ipaddress>\n");
        exit(0);
    }

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        printf("inet_pton error for %s\n", argv[1]);
        exit(0);
    }

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    printf("send msg to server: \n");
    fgets(sendline, MAXLINE, stdin);
    if (send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
        printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
    }

    sleep(30);    // 在这里等待 30s
    close(sockfd);
    exit(0);
}

结果(服务端:ircu-2,客户端:594381):

[chen@hecs-70768 ~]$ netstat -a | grep 70768
tcp        0      0 hecs-70768:ssh          117.61.106.77:36482     ESTABLISHED
tcp        1      0 hecs-70768:59438        hecs-70768:ircu-2       CLOSE_WAIT
tcp        0      0 hecs-70768:ircu-2       hecs-70768:59438        FIN_WAIT2
tcp        0     36 hecs-70768:ssh          117.61.106.77:36526     ESTABLISHED

服务端发起关闭,发包 FIN ,客户端回复 ACK,接着客户端准备发送 FIN
在这个过程,马上关闭了 服务端套接字,服务端没有回复 ACK,那么服务端就处于FIN_WAIT2(因为是服务端主动发起连接的)。而客户端处于CLOSE_WAIT 状态。由于服务端没有回复 ACK ,客户端就重发它的 FIN ,一共重发 5 词(这个大概看系统)。还是没有回复(因为服务端挂了),此时客户端发起一个 RST,将连接断开。在这里插入图片描述
之后服务端的连接就关闭了,就进入到TIME_WAIT状态。

tcp        0      0 hecs-70768:ircu-2       hecs-70768:59438        TIME_WAIT

所以,谁发起 close 连接,谁就有FIN_WAIT2TIME_WAIT 的状态,而对端就有 CLOSE_WAIT状态
图示网上的:https://www.cnblogs.com/guohu/p/13379352.html

end

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值