socket网络编程——基础
本文重点介绍 socket 网络编程中的套接字函数,介绍各个函数是什么以及怎么用
一、常用套接字函数介绍
使用套接字函数需要包含头文件 <arpa/inet.h>
1、socket() 函数
// 创建一个套接字
int socket(int domain, int type, int protocol);
参数 domain:使用的地址族协议,AF_INET 为使用 IPv4 格式的 ip 地址,AF_INET6 为使用IPv6格式的 ip 地址,一般选择 IPv4
参数 type:SOCK_STREAM 为使用流式的传输协议(如TCP),SOCK_DGRAM 为使用报式(报文)的传输协议(如UDP),一般选择 TCP
参数 protocol:一般写 0 即可
返回值:成功则返回可用于套接字通信的 fd(文件描述符),失败则返回 -1
2、bind() 函数
// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 sockfd:监听的 fd, 通过 socket() 调用得到的返回值
参数 addr:传入参数, 要绑定的 IP 和端口信息需要初始化到这个结构体中,IP 和端口要转换为网络字节序
参数 addrlen:参数 addr 指向的内存大小, sizeof(struct sockaddr)
返回值:成功则返回 0,失败则返回 -1
3、listen() 函数
// 监听
int listen(int sockfd, int backlog);
参数 sockfd:监听的 fd, 通过 socket() 调用得到的返回值
参数 backlog:同时能处理的最大连接要求,最大值为 128
返回值:函数调用成功返回 0,调用失败返回 -1
4、accept()函数(默认阻塞)
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数 sockfd:监听的 fd, 通过 socket() 调用得到的返回值
参数 addr:传出参数, 存储建立连接的客户端地址信息
参数 addrlen:传入传出参数,存储 addr 指向的内存大小
返回值:成功则得到一个用于通信的 fd,失败则返回 -1
5、read()/recv()函数(默认阻塞)
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
参数 sockfd:用于通信的 fd, accept() 函数的返回值
参数 buf:指向一块内存, 存储接收的数据
参数 size:sizeof(buf)
参数 flags:一般指定为 0
返回值:大于 0 表示实际接收的字节数;等于 0 表示对方断开了连接;等于 -1表示接收数据失败
6、write()/send()函数
// 发送数据
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
参数 fd:通信的 fd, accept() 函数的返回值
参数 buf:传入参数, 要发送的字符串
参数 len:sizeof(buf)
参数 flags:一般指定为 0
返回值:大于 0 表示实际发送的字节数,等于 -1表示发送数据失败
7、connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 sockfd:通信的 fd, 通过调用 socket() 函数就得到了
参数 addr:存储要连接的服务器端的地址信息
参数 addrlen: sizeof(struct sockaddr)
返回值:连接成功则返回0,连接失败返回 -1
二、TCP通信流程
如下图所示:
三、简单的TCP通信代码
服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
void *client_thread(void *arg);
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("Could not create socket");
return 1;
}
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
if (-1 == bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
{
perror("Bind failed");
close(sockfd);
return 1;
}
if (-1 == listen(sockfd, 128))
{
perror("Listen failed");
close(sockfd);
return 1;
}
printf("Listening on port 2000...\n");
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
while (1)
{
int clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len);
if (-1 == clientfd)
{
perror("Accept failed");
continue;
}
printf("Connection accepted: %d\n", clientfd);
pthread_t thid;
if (0 != pthread_create(&thid, NULL, client_thread, &clientfd))
{
perror("Fail to create thread");
close(clientfd);
}
pthread_detach(thid);
}
}
void *client_thread(void *arg)
{
int clientfd = *(int *)arg;
char buffer[1024] = {0};
while (1)
{
int count = recv(clientfd, buffer, sizeof(buffer), 0);
if (count <= 0)
{ // 断开连接或发生错误
if (count == 0)
{
printf("client disconnect: %d\n", clientfd);
}
else
{
perror("recv failed");
}
close(clientfd);
return NULL;
}
printf("RECV[%d]: %s\n", clientfd, buffer);
count = send(clientfd, buffer, count, 0);
if (count < 0)
{
perror("send failed");
close(clientfd);
return NULL;
}
printf("SEND[%d]: %d bytes\n", clientfd, count);
}
}
客户端用NetAssist(网络助手)模拟即可,是个很轻量的软件,有需要的朋友,链接如下:链接: https://pan.baidu.com/s/1tNtUHSNESZ7PTx4V9FQKQQ 提取码: crx9
四、代码测试
编译并运行:
开三个NetAssist连接,收发数据正常,说明代码逻辑没问题
五、总结
其实上述代码就是经典的一请求一线程模型,相比于大家所熟悉的IO多用复用,其实一请求一线程也不是一无是处,某种程度上来说是异步的,将IO就绪与IO收发数据分开,但是,一个请求就需要开一个线程,显然是很浪费资源的,并且 fd 也不好管理,如果有一种轻量级的线程就好了,也就是所谓的协程,这部分内容以后再详细讨论
更多资源可以查看:学习资料