1.windows环境
在C++中,Windows环境下实现socket通信的客户端与服务端的流程如下:
- 创建套接字:使用socket()函数创建一个套接字。
- 绑定套接字:使用bind()函数将套接字与一个地址(IP和端口)绑定在一起。
- 监听连接:使用listen()函数让服务器开始监听客户端的连接请求。
- 主动连接:使用connect()函数建立与指定IP地址和端口号的服务器的连接
- 接受连接:使用accept()函数接受客户端的连接请求。
- 发送和接收数据:使用send()和recv()函数进行数据的发送和接收。
- 关闭套接字:使用closesocket()函数关闭套接字。
- select函数:是一个用于I/O多路复用的函数。它可以同时监控多个socket,当某个socket上有数据可读、可写或者出现异常时,select()函数会返回。这样可以避免阻塞等待某个socket上的事件,提高程序的效率。
socket()函数
用于创建套接字的函数
#include <winsock2.h>SOCKET socket(int af, int type, int protocol);
参数说明:
- af:地址族(Address Family),表示所使用的协议族。常用的值有AF_INET(IPv4协议)和AF_INET6(IPv6协议)。
- type:套接字类型,表示套接字的功能。常用的值有SOCK_STREAM(面向连接的流式套接字,如TCP)和SOCK_DGRAM(无连接的数据报套接字,如UDP)。
- protocol:协议类型,通常设置为0,让系统自动选择合适的协议。
返回值:
- 如果成功,返回一个套接字描述符(非负整数)。
- 如果失败,返回INVALID_SOCKET(-1)。
可能遇到的问题:
- 在使用socket()函数之前,需要先调用WSAStartup()函数初始化Winsock库。
- 在使用完套接字后,需要调用closesocket()函数关闭套接字。
- 在使用socket()函数时,可能会遇到错误,可以通过调用WSAGetLastError()函数获取错误代码。
bind()函数
用于将套接字与特定的IP地址和端口号绑定。具体定义如下:
#include <sys/types.h>
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
- sockfd:套接字描述符,由socket()函数创建。
- addr:指向struct sockaddr结构体的指针,包含了要绑定的IP地址和端口号信息。
- addrlen:addr指针所指向的结构体的大小。
返回值:
- 成功:返回0。
- 失败:返回-1,并设置errno为相应的错误码。
可能遇到的问题:
- 如果传入的sockfd不是有效的套接字描述符,bind()函数将返回-1,并设置errno为EBADF(Bad file number)。
- 如果传入的addr指针为NULL,bind()函数将返回-1,并设置errno为EINVAL(Invalid argument)。
- 如果传入的addrlen小于实际结构体大小,bind()函数将返回-1,并设置errno为EINVAL(Invalid argument)。
- 如果传入的IP地址和端口号已经被其他套接字绑定,bind()函数将返回-1,并设置errno为EADDRINUSE(Address already in use)。
- 如果传入的IP地址不在本地主机上,bind()函数将返回-1,并设置errno为EADDRNOTAVAIL(Cannot assign requested address)。
listen()函数
listen()函数用于监听来自客户端的连接请求。它是TCP服务器端套接字的一个函数,用于设置套接字为监听模式,以便接受客户端的连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数:
- sockfd:一个已绑定到本地地址(使用bind()函数)的服务端套接字描述符。
- backlog:允许挂起的最大连接数。通常设置为5或更大的值。
返回值:
- 成功:返回0。
- 失败:返回-1,并设置errno。
可能遇到的问题:
- 如果sockfd不是一个有效的套接字描述符,listen()函数将返回-1,并设置errno为EBADF。
- 如果sockfd未绑定到一个本地地址,listen()函数将返回-1,并设置errno为EINVAL。
- 如果backlog小于0,listen()函数将返回-1,并设置errno为EINVAL。
- 如果系统资源不足,listen()函数将返回-1,并设置errno为EAGAIN或ENOMEM。
connect()函数
#include <winsock2.h>int connect(SOCKET s,const struct sockaddr *name,int namelen
);
参数说明:
s
:一个有效的套接字描述符,由socket()函数创建。name
:指向一个sockaddr结构的指针,该结构包含了要连接的服务器的地址信息。namelen
:name参数指向的结构的大小(以字节为单位)。
返回值:
- 如果成功,返回0;如果失败,返回SOCKET_ERROR。可以使用WSAGetLastError()函数获取错误代码。
可能遇到的问题:
- 网络不可用或无法访问目标服务器。
- 目标服务器未运行或未监听指定的端口。
- 连接超时或被拒绝。
- 本地计算机上的防火墙设置阻止了连接。
- 本地计算机上的网络配置问题。
accept()函数
accept()函数用于接受一个已连接的客户端请求。它是在服务器端使用的,用于从已完成连接队列中取出一个连接请求,并创建一个新的套接字与客户端进行通信。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>SOCKET accept(SOCKET s,struct sockaddr *addr,int *addrlen
);
参数说明:
- s:服务器端的套接字描述符,通常是通过socket()函数创建的。
- addr:指向一个sockaddr结构体的指针,用于存储客户端的地址信息。
- addrlen:指向一个整数的指针,表示addr缓冲区的大小。在调用accept()之前,需要将addrlen设置为addr缓冲区的大小;在调用accept()之后,addrlen将被设置为实际接收到的客户端地址信息的长度。
返回值:
- 如果成功,accept()函数返回一个新的套接字描述符,用于与客户端进行通信。
- 如果失败,返回INVALID_SOCKET(通常为-1)。
可能遇到的问题:
- 如果在调用accept()之前没有调用bind()和listen()函数,将无法接受客户端的连接请求。
- 如果客户端连接请求的数量超过了系统允许的最大值,accept()函数可能会阻塞,直到有可用的连接请求为止。
- 如果客户端连接请求被拒绝,accept()函数将返回错误代码。
select()函数
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
- nfds:需要监控的文件描述符的最大值加1。
- readfds:需要监控可读事件的socket文件描述符集合。
- writefds:需要监控可写事件的socket文件描述符集合。
- exceptfds:需要监控异常事件的socket文件描述符集合。
- timeout:select()函数的超时时间,如果设置为NULL,则表示永远等待。
返回值:
- 成功:返回就绪的文件描述符个数。
- 超时:返回0。
- 出错:返回-1。
可能遇到的问题:
- 每次调用select()函数时,都需要重新设置文件描述符集合,这可能会导致效率较低。
- select()函数只能处理最多1024个文件描述符,如果需要处理更多的文件描述符,可以使用poll()函数。
- select()函数在Windows环境下不可用,可以使用WSAEventSelect()函数替代。
示例
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>#pragma comment(lib, "Ws2_32.lib")int main() {WSADATA wsaData;WSAStartup(MAKEWORD(2, 2), &wsaData);SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);serverAddr.sin_port = htons(8888);bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));listen(listenSocket, 5);fd_set readfds;FD_ZERO(&readfds);FD_SET(listenSocket, &readfds);while (1) {struct timeval timeout;timeout.tv_sec = 5;timeout.tv_usec = 0;int ret = select(0, &readfds, NULL, NULL, &timeout);if (ret == SOCKET_ERROR) {printf("select error: %d", WSAGetLastError());break;} else if (ret == 0) {printf("select timeout");continue;}for (int i = 0; i < listenSocket + 1; i++) {if (FD_ISSET(i, &readfds)) {if (i == listenSocket) {sockaddr_in clientAddr;int clientAddrLen = sizeof(clientAddr);SOCKET clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &clientAddrLen);FD_SET(clientSocket, &readfds);printf("new connection: %d", clientSocket);} else {char buffer[1024];int recvLen = recv(i, buffer, sizeof(buffer) - 1, 0);if (recvLen <= 0) {closesocket(i);FD_CLR(i, &readfds);printf("connection closed: %d", i);} else {buffer[recvLen] = '\0';printf("received from %d: %s", i, buffer);}}}}}closesocket(listenSocket);WSACleanup();return 0;
}
send()函数
send()函数用于发送数据
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
- sockfd:表示要发送数据的套接字描述符。
- buf:指向要发送数据的缓冲区。
- len:要发送数据的长度。
- flags:可选参数,用于设置发送数据的行为,如阻塞或非阻塞等。常用的标志有:MSG_DONTROUTE(不查找路由表)、MSG_OOB(发送带外数据)等。
返回值:
- 成功时,返回实际发送的字节数。
- 失败时,返回-1,并设置errno为相应的错误码。
可能遇到的问题:
- 发送缓冲区满:当发送缓冲区满时,send()函数会阻塞,直到有足够的空间可用。可以通过设置socket选项SO_SNDBUF来调整发送缓冲区的大小。
- 网络中断:当网络连接中断时,send()函数可能会返回-1,并设置errno为EINTR。此时,可以尝试重新发送数据。
- 地址不可达:当目标地址不可达时,send()函数会返回-1,并设置errno为EHOSTUNREACH。需要检查目标地址是否正确。
- 超时:当发送操作超时时,send()函数会返回-1,并设置errno为EAGAIN或EWOULDBLOCK。可以通过设置socket选项SO_SNDTIMEO来设置发送超时时间。
recv()函数
用于接收从指定的socket发送过来的数据
#include <winsock2.h>int recv( SOCKET s, char *buf, int len, int flags);
参数说明:
- s:指定要接收数据的socket。
- buf:指向接收到的数据的缓冲区。
- len:指定要接收的最大字节数。
- flags:通常设置为0,表示使用默认操作。
返回值:
- 如果成功,返回接收到的字节数;
- 如果失败,返回SOCKET_ERROR,可以通过调用WSAGetLastError()函数获取错误代码。
可能遇到的问题:
- 网络连接断开或对方关闭连接,recv()函数会返回0,表示连接已关闭。
- 阻塞模式下,如果没有数据可接收,recv()函数会一直等待,直到有数据到达或发生错误。
- 非阻塞模式下,如果没有数据可接收,recv()函数会立即返回SOCKET_ERROR,错误代码为WSAEWOULDBLOCK。
- 缓冲区大小不足以容纳接收到的数据,recv()函数会截断数据并返回实际接收到的字节数。
- 网络延迟或丢包可能导致数据接收不完整或顺序错乱。
代码示例
服务端
#include <iostream>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib") // Winsock Library#define BUF_SIZE 100
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 1234
int main() {WSADATA wsaData;SOCKET hServSock, hClntSock;char buffer[BUF_SIZE];int strLen;SOCKADDR_IN servAddr, clntAddr;int clntAddrSize;// 初始化Winsockif (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {std::cerr << "WSAStartup failed.\n";return -1;}// 创建套接字hServSock = socket(AF_INET, SOCK_STREAM, 0);if (hServSock == INVALID_SOCKET) {std::cerr << "Socket creation failed.\n";return -1;}// 绑定套接字memset(&servAddr, 0, sizeof(servAddr));servAddr.sin_family = AF_INET;servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);servAddr.sin_port = htons(SERVER_PORT);if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) {std::cerr << "Bind failed.\n";return -1;}// 监听套接字if (listen(hServSock, 5) == SOCKET_ERROR) {std::cerr << "Listen failed.\n";return -1;}// 接受客户端的连接请求clntAddrSize = sizeof(clntAddr);hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &clntAddrSize);if (hClntSock == INVALID_SOCKET) {std::cerr << "Accept failed.\n";return -1;}// 接收并打印数据while ((strLen = recv(hClntSock, buffer, BUF_SIZE, 0)) != 0) {buffer[strLen] = '\0';std::cout << "Received: " << buffer << std::endl;}// 关闭套接字closesocket(hClntSock);closesocket(hServSock);WSACleanup();return 0;
}
客户端
#include <iostream>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib") // Winsock Library#define BUF_SIZE 100
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 1234
int main() {WSADATA wsaData;SOCKET hSocket;char buffer[BUF_SIZE];int strLen;SOCKADDR_IN servAddr;// 初始化Winsockif (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {std::cerr << "WSAStartup failed.\n";return -1;}// 创建套接字hSocket = socket(PF_INET, SOCK_STREAM, 0);if (hSocket == INVALID_SOCKET) {std::cerr << "Socket creation failed.\n";return -1;}// 设置服务器地址结构体memset(&servAddr, 0, sizeof(servAddr));servAddr.sin_family = AF_INET;servAddr.sin_addr.s_addr = inet_addr(SERVER_IP);servAddr.sin_port = htons(SERVER_PORT);// 连接到服务器if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) {std::cerr << "Connect failed.\n";return -1;}// 发送数据while (true) {std::cin >> buffer;send(hSocket, buffer, strlen(buffer), 0);}// 关闭套接字closesocket(hSocket);WSACleanup();return 0;
}
编译
g++ server.cpp -o server -lws2_32
g++ client.cpp -o client -lws2_32
2.Linux环境
在C语言的Linux环境下,实现socket通信以实现进程间通信,可以分为以下几个步骤:
- 创建socket
- 绑定地址和端口
- 监听连接
- 发送和接收数据
创建socket
需要包含头文件<sys/types.h>
、<sys/socket.h>
和<netinet/in.h>
。然后,使用socket()
函数创建一个socket
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>int main() {int server_sockfd;server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
}
绑定地址和端口
使用bind()
函数将socket与指定的地址和端口绑定
#include <arpa/inet.h>struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
监听连接/主动连接
使用listen()
函数开始监听客户端的连接请求
listen(server_sockfd, 5);
使用connect()函数用于建立与指定IP地址和端口号的服务器之间的连接
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd
:套接字描述符,由socket()函数创建。addr
:指向目标服务器的地址结构体指针,通常使用struct sockaddr_in
来表示IPv4地址。addrlen
:地址结构体的大小,对于struct sockaddr_in
,其大小为sizeof(struct sockaddr_in)
。
返回值:
- 成功:返回0,表示连接已建立。
- 失败:返回-1,表示连接失败,可以通过
errno
获取错误码。
发送和接收数据
服务端使用accept()
函数接受客户端的连接请求,然后使用send()
和recv()
函数进行数据的发送和接收。
#include <unistd.h>struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_addr_len);char buffer[1024];
send(client_sockfd, "Hello, client!", strlen("Hello, client!"), 0);
recv(client_sockfd, buffer, sizeof(buffer), 0);
代码示例
服务端
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>int main() {// 创建socketint server_fd = socket(AF_INET, SOCK_STREAM, 0);// 定义sockaddr_in结构体struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888); // 端口号server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址// 绑定socketbind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));// 监听socketlisten(server_fd, 5);// 接受客户端连接struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);// 向客户端发送数据char msg[] = "Hello, client!";send(client_fd, msg, strlen(msg), 0);// 关闭socketclose(client_fd);close(server_fd);return 0;
}
客户端
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>int main() {// 创建socketint client_fd = socket(AF_INET, SOCK_STREAM, 0);// 定义sockaddr_in结构体struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888); // 端口号server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // IP地址// 连接服务器connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));// 接收服务器发送的数据char buffer[1024] = {0};recv(client_fd, buffer, sizeof(buffer), 0);printf("Received from server: %s\n", buffer);// 关闭socketclose(client_fd);return 0;
}