Socket实现简单聊天程序

近期学完TCP/IP协议,东拼西凑写了一个简单Socket程序。在此总结一下,希望总结完成之后能领悟一些东西。

1.什么是Socket?

要了解这个问题首先来看一张图,

其实Socket,就是一组函数,它们和Unix I/O 函数结合起来,用以创建网络应用。由图可以看出Socket介于应用层和运输层之间,是一组接口。它把复杂的TCP/IP协议族隐藏在Socket接口中,给用户看到的只有一组接口。

2.进程之间的通信

学完TCP/IP协议都知道,从IP层来说,通信的两端是两台主机,但是实际上真正进行通信的实体是在主机中的进程,是这台主机的一个进程和另一个主机的一个进程之间交换数据,端到端的通信其实是应用进程之间的通信。在网络层,IP地址唯一标识一台主机,而运输层中的"协议+端口"可以唯一标识一个主机中应用进程,因此"IP地址+协议+端口"其实就可以唯一标识网络中的一个应用进程了。

3.Socket的一些基本操作

3.1 socket()函数

定义:

int socket (int domain, int type, int protocol);  // 成功返回描述符, 出错返回-1
  1. domain是地址族常用的有AF_INET和AF_INET6分别代表IPv4和IPv6的地址。
  2. type为数据传输方式/套接字类型,常用的有SOCK_STREAM(流格式套接字/面向连接的套接字和SOCK_DGRAM(数据报套接字/无连接的套接字。这里又是另一块内容,简言之,TCP使用的是SOCK_STREAM,UDP使用的是SOCK_DGRAM。
  3. protocol表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

注意:当protocol为0时,会自动选择type类型对应的默认协议。
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,我的理解一个socket描述字就像一个特定文件一样,把它作为参数,通过它来进行一些读写操作,我认为type就相当于指定了文件读写的方法一样。

3.2 bind()函数

定义:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  // 成功返回0,出错返回-1
  1. sockfd指的就是socket()函数调用之后返回的那个socket描述符,唯一标识一个描述符。
  2. addr指针指的是要绑定给sockfd的的协议地址。IPv4对应的地址结构有两种。分别是:
struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};
struct in_addr {
        union {
                struct { Uunsigned char s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { unsigned short s_w1,s_w2; } S_un_w;
                unsigned long S_addr;
        } S_un;
};
struct sockaddr {
        u_short sa_family;              
        char    sa_data[14];            
};

这两种地址有何不同呢,通过观察可以发现,这两个结构都是16字节的存储空间(因此很容易由sockaddr_in 转换成为sockaddr),但是sockaddr_in中有端口可以由程序员指定,而sockaddr中并没有可以指定端口的定义,因此建议程序员使用sockaddr_in,在需要使用sockaddr时将sockaddr_in转换为sockaddr
3. addrlen地址的长度(这里要留个坑,是一个细节)。

bind()函数的作用是什么呢?通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

3.3 listen()函数(仅由TCP服务器调用)

定义:

int listen(int sockfd, int backlog);   // 成功返回0, 出错返回-1

1.sockfd套接字,表明被监听的套接字描述符。
2.backlog,表明相应队列要排队的未完成请求的数量。(这里先留个坑,随后补上)

那么listen是怎么工作的呢?
listen把sockfd从主动套接字转化为一个被动套接字(使用socket()函数,默认生成一个主动套接字),使套接字可以接受来自客户端的请求。

3.4 accept()函数(仅由TCP服务器调用)

定义:

int accept(int listenfd, struct sockaddr*addr, int *addrlen); // 成功返回连接描述符, 出错返回-1
  1. listenfd,被监听的套接字才能调用accept()。
  2. addr,客户端的addr,是被填写的addr,因此使用前要先创建一个空addr。
  3. addrlen,一个指向地址长度的指针,是被写的。

注意看注释,返回的是一个已连接描述符,并不是客户端的描述符,这里要画重点了,下面说connect()的时候会详细说明。
accept的作用?accept函数等待来自客户端的连接请求到达被动描述符,然后在addr中填写客户端的套接字地址,并返回一个连接描述符。

3.4 connect()函数(由客户端发起请求)

定义:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);   //  成功返回0, 出错返回-1

这个函数是由客户端发起的,其实不用详细说明,根据前面的解释,用于连接服务器和accept()建立联系的函数。
这里要强调的有一点,就是和accept()之间建立联系的过程,这里由一张图给出。

connfd就是accept()返回的连接套接字。

3.5 read()和write()等函数

接下来是读写函数了,有很多组,这里只介绍两组分别是

recv()和send()
int recv(int sock, char *buf, int len, int flags);   // 成功返回读取的字节数,失败返回-1

int send( int sock, const char *buf, int len, int flags );   // 成功返回发送字节数,失败返回-1
  1. sock指定接收端套接字描述符(即连接描述符)
  2. buf指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
  3. len指明buf的长度
  4. flags一般置0
recvfrom()和sendto()
int recvfrom(int sock, const char *buf, int len, int flags,  const struct sockaddr* from, int* fromlen)   // 成功则返回接收到的字节数,失败返回-1

int sendto(int sock, const char *buf, int len, int flags,  const struct sockaddr* to, int tolen)     // 成功返回发送的字节数,失败返回-1

前四个参数和recv一样,这里不做赘述了,recvfrom会从接受端套接字接收数据并且会得到发送方的套接字地址,而sendto会从发送端发送数据给需要接收的地址。
这两组函数对于TCP和UDP来说其实都是可用的,UDP也可以connect之后使用recv和send,(又给自己挖了个坑,以后有时间补上)。
接下来是TCP版完整程序,首先是TCP的接发数据的流程,如图

#include <winsock.h>
#pragma comment(lib,"WS2_32") // 链接到WS2_32.lib
#include <stdlib.h>
// 用来初始化winsock
class InitSock
{
public:
    InitSock(BYTE minorVer = 2, BYTE majorVer = 2)
    {
        // 初始化WS2_32.dll
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(minorVer, majorVer);
        if (::WSAStartup(sockVersion, &wsaData) != 0)    // 返回0表示正常情况,否则退出程序
        {
            exit(0);
        }
    }
    ~InitSock()
    {
        ::WSACleanup();
    }
};

使用winsock时必须要初始化,这段代码将初始化封装在一个类里面,程序开始前创建一个对象就行了。(关于#pragma comment(lib,"WS2_32") 在这里留个坑,以后有空再说)
TCPServer.cpp

#include "InitSock.h"
#include <iostream>
#include <string>
InitSock sock;
int main() {
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sListen == INVALID_SOCKET) {
        std::cout << "Failed socket()" << std::endl;
        return -1;
    }
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(4567);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;    // inet_addr("0.0.0.0");

    // 绑定这个套接字到一个本地地址
    if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) {
        std::cout << "Failed bind()" << std::endl;
        return -1;
    }
    // 进入监听模式
    if (::listen(sListen, 2) == SOCKET_ERROR)
    {
        printf("Failed listen() \n");
        return -1;
    }
    // 循环接受客户的连接请求
    sockaddr_in remoteAddr;
    int nAddrLen = sizeof(remoteAddr);
    SOCKET sClient = 0;
    char szText[] = " TCP Server Demo! \r\n";

    while (sClient == 0)
    {
        // 接受一个新连接
        sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);    // 会阻塞等待直到连接成功后返回一个套接字
        if (sClient == INVALID_SOCKET)
        {
            printf("Failed accept()");
        }

        std::cout << "接收到一个连接:" << inet_ntoa(remoteAddr.sin_addr) << std::endl;
        continue;
    }

    while (TRUE)
    {
        // 从客户端接收数据
        char buff[256];
        int nRecv = ::recv(sClient, buff, 256, 0);
        if (nRecv > 0)
        {
            buff[nRecv] = '\0';
            std::cout << "接收到的数据:" << buff << std::endl;
        }
        // 向客户端发送数据
        std::cin >> szText;
        ::send(sClient, szText, strlen(szText), 0);

    }

    // 关闭同客户端的连接
    ::closesocket(sClient);

    // 关闭监听套节字
    ::closesocket(sListen);

    return 0;
}

TCPClient.cpp

#include <iostream>
#include "InitSock.h"
#include <string>
InitSock sock;   // 初始化winsock 库
int main()
{
	SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);      // 创建套接字
	if (s == INVALID_SOCKET) {
		std::cout << "Failed socket()" << std::endl;
		return -1;
	}
	// 也可以在这里调用bind函数绑定一个本地地址
	// 否则系统将会自动安排
	sockaddr_in addr;   // 远程地址
	addr.sin_family = AF_INET;
	// 填写的是服务器程序的ip地址
	addr.sin_port = htons(4567);   // 用来保存端口号
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (::connect(s, (sockaddr*)&addr, sizeof(addr)) == -1) {
		std::cout << "Failed connect()" << std::endl;
		return -1;
	}
	char buff[256];
	char text[256];
	while (true) {
		// 向服务端发送数据
		std::cin >> text;
		::send(s, text, strlen(text), 0);
		int nrecv = ::recv(s, buff, 256, 0);
		if (nrecv > 0) {
			buff[nrecv] = '\0';
			std::cout << "接收到数据:" << buff << std::endl;
		}
	}
	::closesocket(s);
	return 0;
}

接下来时UDP版的:

UDPServer.cpp

#include <iostream>
#include "InitSock.h"
InitSock sock;
int main() {
	SOCKET sock_Server = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (sock_Server == INVALID_SOCKET)
	{
		std::cout << "Failed socket()" << std::endl;
		return -1;
	}
	sockaddr_in ser_addr;
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(8888);
	ser_addr.sin_addr.S_un.S_addr = INADDR_ANY;      // "0.0.0.0" 
	// UDP不需要监听
	if (::bind(sock_Server, (LPSOCKADDR)&ser_addr, sizeof(ser_addr)) == SOCKET_ERROR) {
		std::cout << "Failed bind()" << std::endl;
		return -1;
	}
	char sendmsg[256];
	char recvmsg[256];
	sockaddr_in cli_addr;
	int len = sizeof(cli_addr); 
	while (TRUE) {
		int count = recvfrom(sock_Server, recvmsg, 256, 0, (sockaddr*)&cli_addr, &len);
		if (count == -1) {
			std::cout << "Recive data fail!" << std::endl;
			return -1;
		}
		std::cout << "收到的数据:" << recvmsg << std::endl;			// 打印收到的数据
		std::cin >> sendmsg;
		sendto(sock_Server, sendmsg, 256, 0, (sockaddr*)&cli_addr, len);
	}
	::closesocket(sock_Server);
	return 0;
}

UDPClient.cpp

#include <iostream>
#include "InitSock.h"
#include <string>
InitSock sock;   // 初始化winsock 库
int main()
{
	SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);      // 创建套接字
	if (s == INVALID_SOCKET) {
		std::cout << "Failed socket()" << std::endl;
		return -1;
	}
	// 也可以在这里调用bind函数绑定一个本地地址
	// 否则系统将会自动安排
	sockaddr_in addr;   // 远程地址
	addr.sin_family = AF_INET;
	// 填写的是服务器程序的ip地址
	addr.sin_port = htons(4567);   // 用来保存端口号
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (::connect(s, (sockaddr*)&addr, sizeof(addr)) == -1) {
		std::cout << "Failed connect()" << std::endl;
		return -1;
	}
	char buff[256];
	char text[256];
	while (true) {
		// 向服务端发送数据
		std::cin >> text;
		::send(s, text, strlen(text), 0);
		int nrecv = ::recv(s, buff, 256, 0);
		if (nrecv > 0) {
			buff[nrecv] = '\0';
			std::cout << "接收到数据:" << buff << std::endl;
		}
	}
	::closesocket(s);
	return 0;
}
posted @ 2020-11-30 19:03  Beyondcoder  阅读(896)  评论(0编辑  收藏  举报