文章目录
- 需要用到的函数
- 1、int socket (int __domain, int __type, int __protocol);
- 2、 int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
- 3、int listen (int __fd, int __n);
- 4、int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t * __restrict __addr_len);
- 5、ssize_t send (int __fd, const void * __buf, size_t __n, int __flags);
- 6、ssize_t recv (int __fd, void * __buf, size_t __n, int __flags);
- 7、int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
- 服务端代码
- 问题总结
需要用到的函数
1、int socket (int __domain, int __type, int __protocol);
1. __domain为地址族(Address Family),IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF_INET 表示IPv4 地址。
2. __type为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
3. __protocol表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议,正常情况下该参数为0,前提是参数1和2能确定唯一的协议信息。
4. int返回值是文件描述符,是从3开始的。0,1,2分别是该进程内部使用的stdin(输入),stdout(输出),stderr(错误)。
该函数的详细介绍:https://blog.csdn.net/qq_53511990/article/details/130659630
2、 int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
1. __fd为 socket 文件描述符。
2. __addr为 sockaddr 结构体变量的指针。
3. __len为 addr 变量的大小,可由 sizeof() 计算得出。
使用的struct sockaddr_in结构体:
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
struct in_addr{
in_addr_t s_addr; //32位的IP地址
};
3、int listen (int __fd, int __n);
socket进入监听状态,此时就可以监听并接受客户端的连接(是由tcp/ip协议栈来监听的)。
__fd为需要进入监听状态的套接字,__n为请求队列的最大长度。
4、int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t * __restrict __addr_len);
1. __fd是服务器端的套接字
2. __addr用来接收保存连接上的客户端的IP地址和端口号
3. __addr_len是__addr长度的变量的指针
4. 返回值int是客户端文件描述符,每连接一个socket,返回的值是依次递增的(4,5,6....),需要注意的是,如果有客户端关闭连接了,再次有新的客户端连接进来时,accept返回的是没有使用的最小的描述符,
比如现在有4,5,6客户端建立了连接,4和5客户端断开了,再次有新的客户端建立连接后,accept会给分配4描述符,这点内核还是很人性化的。
当套接字处于监听状态后,可以通过 accept() 函数来接收客户端请求。
accept() 返回一个新的套接字来和客户端通信,**后面和客户端通信时,要使用这个新生成的套接字。**
注意:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇
到 accept()。
accept() 会阻塞程序执行(后面代码不能被执行),直到有新的客户端连接到来。
5、ssize_t send (int __fd, const void * __buf, size_t __n, int __flags);
1. __fd为要发送数据的套接字
2. buf 为要发送的数据的缓冲区地址
3. __n为要发送的数据的字节数
4. flags 为发送数据时的选项
6、ssize_t recv (int __fd, void * __buf, size_t __n, int __flags);
- __fd
- 客户端或服务端的socket,每个客户端对应唯一的socket
- buf
- 客户端消息的存储空间,也就是个字符数组
- 一般为1500字节
- 网络传输的最大单元,1500字节,也就是客户端发过来的数据,一次最大就是1500字节,这是协议规定,这个数值也是根据很多情况,总结出来的最优值
- 所以客户端最多一组来1500字节,咱们这头1500读一次,就够了。
- __n
- 想要读取到字节个数
- 想要读取到字节个数
- flags
-
一般设置为0
-
MSG_PEEK:窥视传入的数据。 数据被复制到缓冲区中,但不会从输入队列中删除。
-
MSG_OOB:处理带外(OOB)数据。
-
MSG_WAITALL:
仅当发生以下事件之一时,接收请求才会完成:
- 调用方提供的缓冲区已完全满。
- 连接已关闭。
- 该请求已被取消或发生错误。
-
- 返回值
- 读出来的字节大小
- 客户端下线,返回0
- 释放客户端socket
- 释放客户端socket
- 执行失败,返回-1
- 重启
- 等待
- 不用理会
- 读出来的字节大小
7、int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
客户端连接服务器使用,与bind类似。
服务端代码
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
using namespace std;
void *client_thread(void *arg) {
int clientfd = *((int*)arg);
while (1) {
char buf[128] = {0};
//接收数据, 阻塞函数,直到收到客户端发来的数据,代码继续往下执行
int count = recv(clientfd, buf, sizeof(buf), 0);
if (count <= 0) {
//返回值为0,代表客户端下线,为-1代表执行出错
perror("recv");
break;
}
printf("clientfd: %d, count: %d, buffer: %s\n", clientfd, count, buf);
//发送给客户端
send(clientfd, buf, count, 0);
}
//释放客户端socket
close(clientfd);
return NULL;
}
int main() {
//1.int socket (int __domain, int __type, int __protocol);
//(1)__domain 为地址族(Address Family),IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF_INET 表示 IPv4 地址。
//(2)type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
//(3)protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议,正常情况下该参数为0,前提是参数1和2能确定唯一的协议信息。
//(4)返回值是文件描述符
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (socketfd == -1) {
int err = errno; //获取错误代码
perror("socket创建失败!");
return -1;
}
//地址
sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(sockaddr_in));
serverAddr.sin_family = AF_INET; //地址族(Address Family)
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); ///32位IP地址(htonl:将32位的主机字节序转换成网络字节序)
serverAddr.sin_port = htons(2048); //16位的端口号(htonl:将16位的主机字节序转换成网络字节序)
//绑定
if (-1 == bind(socketfd, (sockaddr*)&serverAddr, sizeof(sockaddr))) {
//perror的工作原理:当一个系统调用或库函数发生错误时,
//通常会将全局变量errno设置为一个特定的错误码。
//perror函数读取errno的值,并根据这个值生成相应的错误描述。
//然后,将错误描述与传入的字符串参数拼接,并输出到标准错误流。
perror("bind");
return -1;
}
//进入监听状态
if (-1 == listen(socketfd, 10)) {
perror("listen");
return -1;
}
//通过创建线程,支持多个客户端连接,一个客户端连接创建一个线程
sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr);
while (1) {
memset(&clientAddr, 0, sizeof(clientAddr));
//接收客户端的连接,阻塞函数,直到有客户端连接上,代码继续往下执行
int clientfd = accept(socketfd, (sockaddr*)&clientAddr, &len);
if (-1 == clientfd) {
perror("accept");
return -1;
}
//来一个连接,创建一个线程。缺点:大量客户端连接时,需要大量的线程,无法满足高并发,出现C10K的问题
pthread_t thid;
pthread_create(&thid, NULL, client_thread, &clientfd);
}
close(socketfd);
getchar();
return 0;
}
问题总结
-
当服务端代码当掉或者调用close()函数主动关闭socket连接时,通过 netstat -anop | grep 端口号 命令来查看该端口的连接状态,会出现TIME_WAIT的状态,这是因为服务器主动关闭连接引起的问题。
-
当客户端关闭连接时,在服务端通过 netstat -anop | grep 端口号 命令来查看该端口的连接状态,会出现CLOSE_WAIT的状态,这是因为服务器还没close()该socket引起的
-
补充: linux下的netstat 命令可以查看当前端口列表及指定端口的连接状态等;详情请见https://www.cnblogs.com/zjdxr-up/p/16428925.html