计算机网络知识点总结(三)Socket基本函数详解,C++ Socket入门实战
在上文中,简单介绍了一下什么是socket以及简单举例Socket如何实现客户端和服务端。
计算机网络知识点总结(二)什么是Socket?它跟TCP/IP的关系是什么?
本文将会为详细地讲述Socket的一些基本函数以及如何简单使用socket搭建一个聊天室。
Socket数据结构
Linux系统下的头文件
#include <sys/types.h>
#include <sys/socket.h>
Windows系统下的头文件
#include <winsock2.h>
#pragma comment(lib,"Ws2_32.lib")
服务端与客户端通信过程
基本函数
1.套接字类型的创建
int socket(int __domain, int __type, int __protocol)
参数介绍
第一个参数domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL。
地址族及含义
AF_INET
IPv4网络协议中采用的地址族
AF_INET6
IPv6网络协议中采用的地址族
AF_LOCAL
本地通信中采用的UNIX协议的地址族(用的少)
第二个参数type:指定socket类型。常用的有SOCKET_RAW,SOCK_STREAM和SOCK_DGRAM。
套接字类型及含义
SOCKET_RAW
原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、ICMP协议。
SOCK_STREAM是数据流,一般为TCP/IP协议的编程。
SOCK_DGRAM是数据报,一般为UDP协议的网络编程;
第三个参数protocol:指定协议。最终采用的协议。常见的协议有IPPROTO_TCP、IPPTOTO_UDP。
如果第二个参数选择了SOCK_STREAM,那么采用的协议就只能是IPPROTO_TCP;
如果第二个参数选择的是SOCK_DGRAM,则采用的协议就只能是IPPTOTO_UDP。
当protocol为0时,会自动选择type类型对应的默认协议。
2.向套接字分配网络类型
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int __fd, const sockaddr *__addr, socklen_t __len)
第一个参数socket文件描述符__fd:即套接字创建时返回的对象,
第二个参数addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。包含通信所需要的相关信息,其即IPv4通用结构体sockaddr,具体如下:
struct sockaddr
{
sa_family_t sin_family; //地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
IPv4套接字地址结构通常也称为“网际套接字地址结构",它以sockaddr_in命名.
在具体传参的时候,会用sockaddr结构体的变体sockaddr_in形式去初始化相关字段,定义在<netinet/in.h>头文件中。该结构体具体形式如下,结构体sockaddr中的sa_data就保存着地址信息需要的IP地址和端口号,对应着结构体sockaddr_in的sin_port和sin_addr字段。
struct sockaddr_in{
sa_family_t sin_family; //前面介绍的地址族
uint16_t sin_port; //16位的TCP/UDP端口号
struct in_addr sin_addr; //32位的IP地址
char sin_zero[8]; //不使用
}
IPv6套接字地址结构在<netinet/in.h>头文件中定义。
struct in6_addr{
unit8_t s6_addr [16]; /*128-bit IPv6 address */
/*network byte ordered * /
}
in6_addr的变体sockaddr_in6结构体具体形式如下,由于ipv6使用得相对较少,这里暂时不作具体介绍。
struct sockaddr_in6{
unit8_t sin6_len; /*length of this struct (28)*/
sa_family_t sin6_family; /*AF_INET6*/
in_port_t sin6_port ; /*transport layer port#*/
/*network byte ordered * /
unit32_t sin6_flowinfo;/*flow information, undefined * /
struct in6_addr sin6_addr; /*IPv6 address * /
unit32-t sin6_scope_id;/*set of interfaces for a scope * /
};
IPv6通用结构体sockaddr_storage
struct sockaddr_storage
{
uint8_tss_len; /* length of this struct (implementation dependent) */
sa_family_tss_fanily ; /* Force desired alignment. */
/* implernentation-dependent elenents to provide:
*a) alignment sufficient to fulfill the alignment requirenents of*all socket address types that the system supports
/* b) enough storage to hold any type of socket address that thesystern supports.*/
};
3.进入等待连接请求状态
给套接字分配了所需的信息后,服务端就可以调用listen()函数对来自客户端的连接请求进行监听(客户端此时要调用connect()函数进行连接)
Listen函数的第一个参数即为要监听的socket套接字,第二个参数为相应socket可以排队的最大连接个数。
Connect函数的第一个参数即为客户端的socket套接字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。
4.处理服务端的连接请求
int accept(int __fd, sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)
函数成功执行时返回socket文件描述符,失败时返回-1。
accept函数的第一个参数为服务器的socket描述字,
第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,
第三个参数为协议地址的长度。
accept函数返回的是已连接的socket套接字。
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
SYN:同步序列编号(Synchronize Sequence Numbers)
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据。
由于 TCP 的半关闭(half-close)特性,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
简单来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。
5.write/send发送信息
Linux下的发送函数为:
ssize_t write (int __fd, const void *__buf, size_t __n) ;
而在Windows下的发送函数为:
ssize_t send (int sockfd, const void *buf, size_t nbytes, int flag) ;
第四个参数是传输数据时可指定的信息,一般设置为0。
6.read/recv接受信息
linux下的接收函数为
ssize_t recv(int __fd, void *__buf, size_t __n, int __flags)
而在windows下的接收函数为
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flag) ;
第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0
7.关闭连接
int close(int __fd)
退出连接,此时要注意的是:调用close()函数即表示向对方发送了EOF结束标志信息。
8.C/C++代码实战
以下代码仅供参考,且在Linux环境下运行,需要先运行服务端再运行客户端,仅能实现客户端给服务端发送信息的简单功能。
(已更新实现“伪”半双工程序的的代码)链接如下:
计算机网络知识点总结(四)Linux C++ Socket实现“伪”半双工聊天室程序
已更新C++ Socket实现并发服务器(2022/5/12),可移步至原文。
计算机网络知识点总结(六)Linux C++ Socket实现并发TCP服务器(fork) - 知乎 (zhihu.com)
client.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define MYPORT 7000
#define BUFFER_SIZE 1024
int main()
{
int sockCli = socket(AF_INET, SOCK_STREAM, 0);
///定义sockaddr_in
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(MYPORT); //服务器端口
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服务器ip,inet_addr用于IPv4的IP转换(十进制转换为二进制)
// 127.0.0.1是本地预留地址
//连接服务器,成功返回0,错误返回-1
if (connect(sockCli, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("connect");
exit(1);
}
char sendbuf[BUFFER_SIZE];
char recvbuf[BUFFER_SIZE];
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{ /*每次读取一行,读取的数据保存在buf指向的字符数组中,成功,则返回第一个参数buf;*/
send(sockCli, sendbuf, strlen(sendbuf), 0); ///发送
if (strcmp(sendbuf, "exit\n") == 0)
break;
// recv(sockCli, recvbuf, sizeof(recvbuf), 0); ///接收
// fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf)); //接受或者发送完毕后把数组中的数据全部清空(置0)
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockCli);
return 0;
}
Server.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <thread>
#include <iostream>
#define PORT 7000
#define QUEUE 20 //连接请求队列
int conn;
void thread_task()
{
}
int main()
{
int sockSer = socket(AF_INET, SOCK_STREAM, 0); //若成功则返回一个sockfd(套接字描述符)
struct sockaddr_in server_sockaddr; //一般是储存地址和端口的。用于信息的显示及存储使用
/*设置 sockaddr_in 结构体中相关参数*/
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT); //将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)
// printf("%d\n",INADDR_ANY);
// INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。
//一般来说,在各个系统中均定义成为0值。
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); //将主机的无符号长整形数转换成网络字节顺序。
if (bind(sockSer, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr)) == -1)
{
perror("bind");
exit(1);
}
if (listen(sockSer, QUEUE) == -1)
{
perror("listen");
exit(1);
}
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
///成功返回非负描述字,出错返回-1
conn = accept(sockSer, (struct sockaddr *)&client_addr, &length);
//如果accpet成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。
// accpet之后就会用新的套接字conn
if (conn < 0)
{
perror("connect");
exit(1);
}
char buffer[1024];
while (1)
{
memset(buffer, 0, sizeof(buffer));
int len = recv(conn, buffer, sizeof(buffer), 0); //从TCP连接的另一端接收数据。
/*该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0*/
if (strcmp(buffer, "exit\n") == 0) //如果没有收到TCP另一端发来的数据则跳出循环不输出
{
break;
}
printf("%s", buffer); //如果有收到数据则输出数据
//必须要有返回数据, 这样才算一个完整的请求
send(conn, buffer, len, 0); //向TCP连接的另一端发送数据。
}
close(conn); //因为accpet函数连接成功后还会生成一个新的套接字描述符,结束后也需要关闭
close(sockSer); //关闭socket套接字描述符
return 0;
}