1、基础知识
socket用于计算机之间的网络通信,无论是构建服务器还是客户端,我们仅需要三个信息,服务器的ip地址,对应进程的端口号,通信协议。
拿到ip地址,便自然知道其ip种类且同时知道该服务器的位置,拿到端口号便知道具体和哪个程序对接。一般而言我们使用ipv4的地址格式,端口号也一般设置得大一些,以防止和服务器原本的应用程序冲突。通信协议则分为流式和报式两个种,它们的代表协议分别是TCP和UDP。
总而言之,只要得到这些信息,服务器就能构建其服务程序,客户端也能准确的找到对应的服务程序。
而socket这个名字,也生动的表达的这样一层意思,把服务器看成一个插排,客户端看成各类用电器,一个插排可以给N个设备供电。
2、服务端
第一步,创建一个套接字,并拿到一个文件描述符,以便捷的表示这个套接字。
第二步,构建一个结构体,让这个结构体绑定各种信息(ip地址,端口号,通信协议)。
第三步,开始准备接受客户端的连接请求。
第四步,处理客户端的连接请求,并根据需求,决定是否要记录客户端的ip和端口后等信息。
最后,根据需要进行读写操作。
下面的例程,每一步具体代码怎么写我大致有标注一下。
//设置ip和端口号
std::string server_ip_address = "xxx.xxx.xxx.xxx";
int server_port = 8888;
//step 1
int fd = socket(AF_INET, SOCK_STREAM, 0);
//step2
sockaddr_in addr; //定义一个结构体
addr.sin_family = AF_INET; //指定ip地址格式
addr.sin_port = htons(server_port); //设置端口
inet_pton(AF_INET, server_ip_address.c_str(), &addr.sin_addr.s_addr); //设置ip地址
bind(fd, (struct sockaddr*)&addr, sizeof(addr)); //绑定信息
//step3
listen(fd, 128);
//step4
sockaddr_in client_addr; //定义一个结构体接受客户端信息
socklen_t clilen = sizeof(client_addr);
int cfd = accept(sfd, (struct sockaddr*)&client_addr, &clilen); //比对信息是否匹配
//如果不需要记录客户端的ip和端口,则直接按下面的方式即可
int cfd = accept(sfd, NULL, NULL);
如果想要具体了解各个函数的参数的意义,直接copy之后搜索即可。
有几个点可以注意一下
1、写代码的时候数据的存储方式是小端存储,即主机字节序。譬如这一句:
int server_port = 8888;
它在内存当中就是主机存储。但是用于网络通讯的时候,又是大端存储(又称网络字节序)。所以需要转化一下,一般用到的函数有:htons, htonl, ntohs, ntohl.
意思也很直白,以htons为例,htons即host to network short。host意为主机字节序,network意为网络字节序,short指的是要转化的字节数,short指的是2个字节,long指的是4个字节(和long int对应的字节数不太一样)。
2、listen(fd, 128);这里的128并不是指最多只能接受128个客户端,而是在listen那一瞬间最多只能处理128个,可以多执行几次listen的。
3、在字符串传输的时候不需要考虑字节序的问题。因为一个字符本身就占一个字节,不存在字节内部被打乱的可能。也就是说,所谓字节序的问题是存在局部的,而不存在于整体中。
举个例子,9这个整数,占四个字节,转化成字节流是长这样的。
小端存储(主机字节序):9:0x00 0x00 0x00 0x09
大端存储(网络字节序):9:0x09 0x00 0x00 0x00(其实比对一下,就可以get到为啥传输的时候变成大端的了)
如果是3和9两个数字分别发过去,则不会,也不可以影响3和9的发送顺序。所以对于字符串"abcd",它的发送顺序就是"a" “b” “c” “d”。又因为它们本身就只占一个字节,自然不会被存储方式干扰。
但是如果一个汉字,它由于占多个字节,倒有可能受到字节序的影响。
总的来说,字节序的问题只出现在一个整体被拆分成N个字节,然后传输之后重组才可能出现。
4、如果cfd不为-1则表示正确和客户端建立连接,可以进行读写操作了。具体代码之后再说。
3、客户端
从客户端的角度来说,其主要任务就是拿到服务器的ip地址,端口号,然后主要去和客户端建立起连接。譬如插线要去匹配插座一样。核心步骤和构建服务器的步骤大同小异,大概如下:
第一步,创建一个套接字,并拿到一个文件描述符,以便捷的表示这个套接字。
第二步,构建一个结构体,让这个结构体绑定服务器的各种信息ip地址,端口号,通信协议)。
第三步,利用以上这些信息尝试去连接服务器,如果构建成功则返回的整形变量是正值。
第四步,根据任务需要进行读写操作。
//step 1:
int fd = socket(AF_INET, SOCK_STREAM, 0);
//step 2:
sockaddr_in saddr;
addr.sin_family = AF_INET;
addr.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip_address.c_str(), &addr.sin_addr.s_addr);
//step 3:
do{
ret = connect(sfd, (struct sockaddr*)&saddr, sizeof(saddr)); //connect是一个非阻塞函数,所以需要用while不断尝试去建立连接
std::cout << "try to connect server...\n";
sleep(1);
}while(ret == -1);
4、读写操作
4.1、读写函数
读用的是write
ssize_t write(int fd, const void *buf, size_t count);
- fd是文件描述符。
- buf被写内容的地址。
- count自然是被写内容的字节数。
- 返回值:如果写入成功,则返回写入的字节数目。如果失败,返回-1;如果正常写入,但没写入任何东西,则返回0。
读操作用的是read函数
ssize_t read(int fd, void *buf, size_t count);
- fd:文件描述符。
- buf:指向读取的内容的指针 。
- count:要读取的字节数 。
- 返回值:如果读取成功,则返回读取的字节数目。如果失败,返回-1;如果正常读取,但没读取到东西,返回0。
其实这里write和read和Linux文件IO操作中的read和write函数是一样的,包括close操作也是。可以看我之前写的文章。
链接: Linux学习笔记之四(文件IO、目录IO)
4.2、阻塞IO和非阻塞IO
其实无论是调用write函数还是read函数,都是在内核空间中的缓存区进行操作。流程大概是,通过write函数把数据写到内核空间的写缓存区,然后内核自动通过一些协议把数据传输到读缓存区,然后read函数才能从读缓存区把数据给读出来。
但是这个读写操作分为阻塞式读写和非阻塞式读写,专业术语叫阻塞IO和非阻塞IO。默认情况下是采用阻塞式IO,在阻值式模式下,write操作如果发现写缓存区已经满了,则会一直在那里等待,直到有多余空间可以写入;对于read操作来说,则是如果发现读缓存区没有数据,则一直阻塞等待,直到有数据出现。
而非阻塞式刚好相反,以read为例,如果发现读缓存区中没有数据,则返回-1,然后继续执行其它任务。
非阻塞可以通过相关函数来设置,自行上网查即可。下面讲讲它们的优缺点。
- 阻塞式IO
- 优点:操作和逻辑都比较简单,确保write和read能正确执行才继续执行其它任务。
- 缺点:浪费线程时间,有可以阻塞的时间过长而影响其它任务的执行。
- 非阻塞式IO
- 优点:可以及时的去执行其它任务,提高线程的利用率。
- 缺点:可以导致读写数据不及时。
总的两说,两者各有优劣。但也有一些方法可以取长补短,比如select,epoll,poll等。暂且不谈了。
5、例程
服务器例程,我将读写分为两个线程来写。
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
std::string server_ip_address = "192.168.52.130";
int server_port = 8888;
int exit_flag = 0;
std::mutex mu;
int ServerInit()
{
//create a socket description
int fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addr;
//get ip type
addr.sin_family = AF_INET;
//get host port
addr.sin_port = htons(server_port);
//get ip iddress
inet_pton(AF_INET, server_ip_address.c_str(), &addr.sin_addr.s_addr);
//link above information to socket
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
return fd;
}
void SendThread(int cfd)
{
std::string meg;
while(true)
{
std::getline(std::cin, meg);
if(meg == "exit"){
write(cfd, meg.c_str(), meg.length());
mu.lock();
exit_flag = 1;
mu.unlock();
break;
}
write(cfd, meg.c_str(), meg.length());
}
}
void ReceThread(int cfd)
{
char buf[1024];
while(exit_flag == 0)
{
//set buff to zero
memset(buf, 0, sizeof(buf));
int len = read(cfd, buf, sizeof(buf));
if(len > 0) std::cout << " receive from client is: " << buf << std::endl;
}
}
int main()
{
//sfd: server file description
int sfd = ServerInit();
//beign to wait for client, block.
listen(sfd, 128);
sockaddr_in client_addr;
socklen_t clilen = sizeof(client_addr);
//receive the information from client, cfd: client file desription
int cfd = accept(sfd, (struct sockaddr*)&client_addr, &clilen);
if(cfd != -1)
{
std::thread send_thread(SendThread, cfd);
std::thread rece_thread(ReceThread, cfd);
send_thread.join();
rece_thread.join();
}
close(cfd);
close(sfd);
return 0;
}
客户端例程
#include <iostream>
#include <stdio.h>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
std::string server_ip_address = "192.168.52.130";
int server_port = 8888;
int ClientInit(sockaddr_in& addr)
{
//create a socket description
int fd = socket(AF_INET, SOCK_STREAM, 0);
//get ip type
addr.sin_family = AF_INET;
//get host port
addr.sin_port = htons(server_port);
//get ip iddress
inet_pton(AF_INET, server_ip_address.c_str(), &addr.sin_addr.s_addr);
return fd;
}
int main()
{
//sfd: server file description, saddr: server address
sockaddr_in saddr;
int sfd = ClientInit(saddr);
int ret;
do{
ret = connect(sfd, (struct sockaddr*)&saddr, sizeof(saddr));
std::cout << "try to connect server...\n";
sleep(1);
}while(ret == -1);
std::cout << "connect server successfully!\n";
while(1)
{
static char buf[1024] = {0};
static std::string str = "hello, i am client.";
write(sfd, str.c_str(), str.size());
int len = read(sfd, buf, sizeof(buf));
if(len > 0){
std::cout << "client receive is: " << buf << std::endl;
memset(buf, 0 , sizeof(buf));
}
else if(len == 0)
{
std::cerr << "lose connection with server.\n";
break;
}
}
close(sfd);
return 0;
}