C++ socket通信

C++ socket通信

服务端

  • 步骤如下:
    1. 根据版本启动socket
    2. 根据返回值验证是否成功启动
    3. 调用socket创建套接字(指定通讯方式)
    4. 调用bind函数将套接字绑定到指定ip地址和端口
    5. 调用listen设定客户端链接数
    6. 调用accept等待客户端的连接
    7. 调用recv和send函数接收和发送数据
    8. 调用closesocket关闭socket
    9. 调用WSACleanup回收资源。

代码如下:



#include <iostream>
#include <Winsock2.h>

using namespace std;

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

int main()
{
	WSADATA wsaData;
	SOCKET sockSrv;
	SOCKADDR_IN addrSrv;
	SOCKADDR_IN addrClient;//用来接受客户端的地址信息
	int len = sizeof(SOCKADDR);
	int err;
	WORD wVersionRequested;//用来保存Winsock库的版本号
	wVersionRequested = MAKEWORD(2, 2);//创建一个包含了请求版本号2.2版本的WORD值
	err = WSAStartup(wVersionRequested, &wsaData);
	if (err != 0)
	{
		cout << "启动使用Winsock DLL失败" << endl;
		return 0;
	}
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
	{
		WSACleanup(); //解除与Socket库的绑定并且释放Socket库所占用的系统资源。
		cout << "启动使用Winsock DLL失败" << endl;
		return 0;
	}
	sockSrv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//创建套接字
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(6000);
	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//将套接字绑定到本地址和指定端口号上
	listen(sockSrv, 5);
	SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
	//多线程从这里开启
	//while循环实际是添加到这里,程序运行到这里会进行中断。跳出中断时,表示有客户端连接。进行多
	//线程编程则可以从这里开启线程(本实例为了达到单线程通信(服务器<->客服端),将消息循环接收放在下
	//面的循环里了),例如聊天软件,可以从这里开启多线程消息处理后将消息转发到sockConn对应的客户端,
	//就形成了两个客户端间的通信
	//addrClient包含了连接的客户端的IP地址
	cout << "欢迎你加入" << endl;
	send(sockConn, "欢迎你加入", 20, 0);//用返回的套接字和客户端进行通信。
	while (true)
	{
		char sendBuf[256];
		char recvBuf[256];
		recv(sockConn, recvBuf, strlen(recvBuf),0);
		Sleep(1000);
		if (strlen(recvBuf) > 0)
		{
			cout << recvBuf << endl;
			cout << "Input:";
			cin >> sendBuf;
			send(sockConn, sendBuf, strlen(sendBuf) + 1, 0);//用返回的套接字和客户端进行通信。
		}
	}
	closesocket(sockConn);
	WSACleanup();
	system("pause");
}


客户端

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include <Winsock2.h>
#include<stdlib.h>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
int main()
{
    WORD wVersinRequested;
    WSADATA wsaData;
    int err;
    wVersinRequested = MAKEWORD(2, 2);
    err = WSAStartup(wVersinRequested, &wsaData);
    if (err != 0)
    {
        cout << "启动使用Winsock DLL失败" << endl;
        return 0;
    }
    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
    {
        WSACleanup();
        cout << "启动使用Winsock DLL失败" << endl;
        return 0;
    }
    SOCKADDR_IN addrSrv;//设定服务器端的IP和端口
    addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//本地回路地址
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(6000);
    SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);//将其第三个参数设为0,让其自动选择协议。
    connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//与服务器建立连接。
    Sleep(2000);
    while (1)
    {
        char recvBuf[100];
        char sendBuf[100];
        recv(sockClient, recvBuf, 100, 0);
        Sleep(1000);
        if (strlen(recvBuf) > 0)
        {
            cout << recvBuf << endl;
            cout << "Input:";
            cin >> sendBuf;
            send(sockClient, sendBuf, strlen(sendBuf) + 1, 0);
        }
    }
    closesocket(sockClient);
    WSACleanup();
    system("pause");
}

重要函数说明

  • WSAStartupWSACleanup
//当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相
//应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调
//用所请求的Socket库中的其它Socket函数了。该函数执行成功后返回0。
int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );

//应用程序在完成对请求的Socket库的使用后,要调用WSACleanup函数来解除与
//Socket库的绑定并且释放Socket库所占用的系统资源
int WSACleanup (void);

  • bind
//服务端用于将把用于通信的地址和端口绑定到 socket 上。所以可以猜出,这个函
//数的参数应该包含:用于通信的 socket 和服务端的 IP 地址和端口号。ip地址和
//端口号是放在 socketaddr_in 结构体里面的。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

//(1)参数 sockfd ,需要绑定的socket。
//(2)参数 addr ,存放了服务端用于通信的地址和端口。
//(3)参数 addrlen ,表示 addr 结构体的大小
//(4)返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误。
  • listen
//只是把套接字从主动变为被动,并限制链接数
//返回:0──成功, -1──失败
int listen(int sockfd, int backlog)
  • accept
//主要用在基于连接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所
//监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向
//该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不
//受该系统调用的影响。
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);    
  • send
//不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
//客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程
//序发送应答。
int send( SOCKET s, const char FAR *buf, int len, int flags );
//返回值>0时,表示实际发送了多少字节。注意:只是copy到系统缓存里,系统决定
//什么时候会发送这些数据。
//返回值==0时,这个在send空串时会发生,是正常的。
//返回值<0时(只会等于-1吧),需要检查errno,当errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN时,连接正常,可以稍后再试。其他的就是连接异常了。



  • recv
int WSAAPI recv(
  SOCKET s,//标识已连接套接字的描述符。
  char   *buf,//指向缓冲区的指针,以接收传入的数据。
  int    len,//buf参数指向的缓冲区的长度(以字节为单位)。
  int    flags//一组影响此功能行为的标志。 
);
//返回值>0时,表示实际接受到多少字节。
//返回值==0时,表示连接断开,也就是收到了FIN或者RST。
//返回值<0时,检查errno,和send类似。
  • connect
//TCP客户端通过connect函数与服务端连接,进行通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//参数:int sockdf:socket文件描述符
//参数: const struct sockaddr *addr:传入参数,指定服务器端地址信息,含IP地址和端口号
//参数:socklen_t addrlen:传入参数,传入sizeof(addr)大小
//返回值:成功: 0   失败:-1,设置errno

  • closesocket
//关闭套接字
closesocket(
    _In_ SOCKET s
    );
  • socket中send和recv函数补充说明

TCP socket的buffer

每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的流量(拥塞)控制便是依赖于这两个独立的buffer以及buffer的填充状态。接收缓冲区把数据缓存入内核,应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。再啰嗦一点,不管进程是否调用recv()读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回,仅此而已。进程调用send()发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send()返回之时,数据不一定会发送到对端去(和write写文件有点类似),send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止。对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。

send函数工作原理

send函数只负责将数据提交给协议层。 当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲区的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR; 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据; 如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len; 如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完,如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。 如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR; 如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

posted @ 2022-10-25 10:22  伊红美兰  阅读(274)  评论(0编辑  收藏  举报