C++实现简单的网络聊天程序

时间:2024-03-03 12:50:00

1、什么是socket,socket在哪?

   Socket是应用层与 TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作,Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写、打开、关闭),这些函数我们在后面进行介绍。

有三种不同形式的套接字:流式套接字(SOCK_STREAM),数据包套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。

基于TCP的Socket使用流式套接字,相比于使用数据包套接字的UDP来讲,TCP可以使程序员不必关心数据正确性及顺序正确性,缺点是效率较低。

基于TCP的Socket编程最常见的应用场景是在C/S架构下的分布式应用,针对客户端和服务器端提供不同的Socket系统调用。

2、client/server(CS)模式

服务端:服务器端: 初始化 socket套接字----->绑定socket----->对端口进行监听(listen)----->阻塞(accept)----->等待客户端连接,至此程序运行到刚启动服务端的状态。

客户端:初始化 socket套接字------>发送连接请求(connect),如果连接成功,客户端发送数据请求,服务器接受请求并处理请求,把回应数据发送给客户端,客户端读取数据,最后关闭连接,完成一次交互。

3、实现代码及相关API介绍

//服务器
#include<iostream>
#include<winsock.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void initialization();
int main() {
    //定义长度变量
    int send_len = 0;
    int recv_len = 0;
    int len = 0;
    //定义发送缓冲区和接受缓冲区
    char send_buf[100];
    char recv_buf[100];
    //定义服务端套接字,接受请求套接字
    SOCKET s_server;
    SOCKET s_accept;
    //服务端地址客户端地址
    SOCKADDR_IN server_addr;
    SOCKADDR_IN accept_addr;
    initialization();
    //填充服务端信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(1234);
    //创建套接字
    s_server = socket(AF_INET, SOCK_STREAM, 0);
    if (bind(s_server, (SOCKADDR*)& server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
        cout << "套接字绑定失败!" << endl;
        WSACleanup();
    }
    else {
        cout << "套接字绑定成功!" << endl;
    }
    //设置套接字为监听状态
    if (listen(s_server, SOMAXCONN) < 0) {
        cout << "设置监听状态失败!" << endl;
        WSACleanup();
    }
    else {
        cout << "设置监听状态成功!" << endl;
    }
    cout << "服务端正在监听连接,请稍候...." << endl;
    //接受连接请求
    len = sizeof(SOCKADDR);
    s_accept = accept(s_server, (SOCKADDR*)& accept_addr, &len);
    if (s_accept == SOCKET_ERROR) {
        cout << "连接失败!" << endl;
        WSACleanup();
        return 0;
    }
    cout << "连接建立,准备接受数据" << endl;
    //接收数据
    while (1) {
        recv_len = recv(s_accept, recv_buf, 100, 0);
        if (recv_len < 0) {
            cout << "接受失败!" << endl;
            break;
        }
        else {
            cout << "客户端信息:" << recv_buf << endl;
        }
        cout << "请输入回复信息:";
        cin >> send_buf;
        send_len = send(s_accept, send_buf, 100, 0);
        if (send_len < 0) {
            cout << "发送失败!" << endl;
            break;
        }
    }
    //关闭套接字
    closesocket(s_server);
    closesocket(s_accept);
    //释放DLL资源
    WSACleanup();
    return 0;
}
void initialization() {
    //初始化套接字库
    WORD w_req = MAKEWORD(2, 2);//版本号
    WSADATA wsadata;
    int err;
    err = WSAStartup(w_req, &wsadata);
    if (err != 0) {
        cout << "初始化套接字库失败!" << endl;
    }
    else {
        cout << "初始化套接字库成功!" << endl;
    }
    //检测版本号
    if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
        cout << "套接字库版本号不符!" << endl;
        WSACleanup();
    }
    else {
        cout << "套接字库版本正确!" << endl;
    }
    //填充服务端地址信息

}
//客户端
#include<iostream>
#include<winsock.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void initialization();
int main() {
    //定义长度变量
    int send_len = 0;
    int recv_len = 0;
    //定义发送缓冲区和接受缓冲区
    char send_buf[100];
    char recv_buf[100];
    //定义服务端套接字,接受请求套接字
    SOCKET s_server;
    //服务端地址客户端地址
    SOCKADDR_IN server_addr;
    initialization();
    //填充服务端信息
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(1234);
    //创建套接字
    s_server = socket(AF_INET, SOCK_STREAM, 0);
    if (connect(s_server, (SOCKADDR*)& server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
        cout << "服务器连接失败!" << endl;
        WSACleanup();
    }
    else {
        cout << "服务器连接成功!" << endl;
    }

    //发送,接收数据
    while (1) {
        cout << "请输入发送信息:";
        cin >> send_buf;
        send_len = send(s_server, send_buf, 100, 0);
        if (send_len < 0) {
            cout << "发送失败!" << endl;
            break;
        }
        recv_len = recv(s_server, recv_buf, 100, 0);
        if (recv_len < 0) {
            cout << "接受失败!" << endl;
            break;
        }
        else {
            cout << "服务端信息:" << recv_buf << endl;
        }

    }
    //关闭套接字
    closesocket(s_server);
    //释放DLL资源
    WSACleanup();
    return 0;
}
void initialization() {
    //初始化套接字库
    WORD w_req = MAKEWORD(2, 2);//版本号
    WSADATA wsadata;
    int err;
    err = WSAStartup(w_req, &wsadata);
    if (err != 0) {
        cout << "初始化套接字库失败!" << endl;
    }
    else {
        cout << "初始化套接字库成功!" << endl;
    }
    //检测版本号
    if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2)
 {
        cout << "套接字库版本号不符!" << endl;
        WSACleanup();
    }
    else {
        cout << "套接字库版本正确!" << endl;
    }
    //填充服务端地址信息

}

linux下和c++中相关API介绍:

1)socket()函数

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

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6,协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址与端口号的组合

type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM

protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP

在c++下,函数形式为

SOCKET PASCAL FAR socket(int af, int type, int protocol);

参数同上。

2)bind()函数

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

sockfd:即socket描述字,它是通过socket()函数创建,唯一标识一个socket。bind()函数就是给这个描述字绑定一个名字

addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址,这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

addrlen:对应的是地址的长度

在c++下,函数形式为

int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);

参数s是欲建立连接的本地套接字描述符,参数name指出说明对方套接字地址结构的指针,对方套接字地址长度由namelen说明。

如果没有错误发生,connect()返回0,否则返回值SOCKET_ERROR。

3)listen()、connect()函数

 int listen(int sockfd, int backlog);

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

对于服务器来说,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

listen()函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket队列中允许的连接数目。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

connect()函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度,客户端通过三次握手来建立与TCP服务器的连接。

在c++下,函数形式为

int PASCAL FAR listen(SOCKET s, int backlog);
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);

listen()中,参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。

connect()中,参数s是欲建立连接的本地套接字描述符,参数name指出说明对方套接字地址结构的指针,对方套接字地址长度由namelen说明。

4)accept()函数

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

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数接收请求,这样连接就建立好了,之后就可以开始I/O操作了。

accept()函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是一个全新的描述字,返回客户的TCP连接。

在c++中,函数形式为

SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);

参数s为本地套接字描述符,在用accept()调用参数前应该先调用过listen()。addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回INVALID_SOCKET。

5)send()、recv()函数

int send(int sockfd, const void *msg, int len, int flags);
int recv(int sockfd, void *buf, int len, unsigned int flags);

send()中sockfd是你想发送数据的套接字描述字,msg 是指向你想发送的数据的指针,len是数据的长度,flags一般设置为0。

recv()中sockfd是要读的套接字描述字,buf是要读的信息的缓冲,len是缓冲的最大长度,flags一般设置为0。

在c++下,函数形式为

int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);

6)close()函数

int close(int fd);

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

在c++下,函数形式为

int PASCAL FAR closesocket ( IN SOCKET s);

4、程序运行结果: