上一篇博客已经介绍了TCP和UDP的的初步认识和socket编程部分API
Linux网络——网络编程的基本知识、socket网络编程接口和UDP网络程序实战
本篇博客将回顾并整理socket编程的相关API
一、TCP的socketAPI详解
1.1 创建 socket 文件描述符 (TCP/UDP)
int socket(int domain, int type, int protocol);
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol参数的介绍从略,指定为0即可
1.2 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
- 我们的程序中对myaddr参数是这样初始化的:
- 将整个结构体清零;
- 设置地址类型为AF_INET;
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用
哪个IP 地址; - 端口号为SERV_PORT, 我们定义为9999
1.3 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
监听类似于等待服务器连接的到来
1.4 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;如果给addr 参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的缓冲区addr的长度,以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)
accept的返回值可以通过一个栗子解释:
拉客的人把你拉进餐厅后就继续去拉下一个客人了,你进了餐厅后会有招待你的服务员。返回值用来连接通信和我们通信
1.5 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
- connect()成功返回0,出错返回-1
二、网络编程常用API
2.1 字节序转换函数
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
说明:在上述的函数中,h代表host;n代表network s代表short;l代表long
2.2 地址转换函数
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
三、TCP网络程序
首先明确下面的概念
客户端都不需要bind,但是需要IP和PORT,原因:
- 进行bind时候容易冲突,导致客户端无法启动的问题!
- 客户端需要唯一性,但是不需要明确是哪个端口号
需要IP和PORT的原因:
- 客户端使用udp协议进行收发的时候,系统会自动进行ip和端口号的绑定
3.1 单进程单线程版本
<1> 服务器端代码
- tcpServer.hpp
#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H
#include<iostream>
#include<string>
#include<unistd.h>
#include<cstring>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#define BACKLOG 5
class tcpServer{
private:
int port; //端口号
int lsock; //监听套接字
public:
//构造函数
tcpServer(int _port)
:port(_port),lsock(-1)
{}
//初始化服务器
void initServer()
{
//1.创建套接字
lsock = socket(AF_INET,SOCK_STREAM,0);
if(lsock < 0)
{
std::cerr << "lsocker error" << std::endl;
exit(2);
}
//填充结构体协议
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
//2.绑定
if(bind(lsock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error" << std::endl;
exit(3);
}
//3.监听(允许在任何时刻有客户端来连接服务器)
//backlog(底层链接队列的长度),例如海底捞排队的长度,全连接队列
if(listen(lsock,BACKLOG) < 0)
{
std::cerr << "bind error" << std::endl;
exit(4);
}
}
//开始服务
void service(int sock)
{
//buf用来接收消息
char buf[1024];
while(true)
{
//read or write来读写信息
//注意这里s的三个取值
//s>0表示收到信息,s==0表示客户端退出,如果不写这个条件或造成服务器一直阻塞在接收或发送的状态,不能再接收客户端信息
ssize_t s = recv(sock,buf,sizeof(buf)-1,0);
if(s > 0)
{
buf[s] = 0;
std::cout << "clinet#" << buf << std::endl;
send(sock, buf, strlen(buf), 0);
}
else if(s == 0)
{
std::cout << "Clinet quit..." << std::endl;
close(sock); //不退出造成描述符越来越少,默认32
break;
}
else{
std::cout << "recv client data error" <<std::endl;
break;
}
}
}
//启动服务器
void start()
{
//定义远端结构体便于获取ip端口等信息
sockaddr_in endpoint;
while(true)
{
socklen_t len = sizeof(endpoint);
//4.接收请求,注意lsock和sock的区别,后面主要为sock服务,lsock只是不断接收客户端
int sock = accept(lsock, (struct sockaddr*)&endpoint, &len);
if(sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
//获取客户端ip和port,inet_ntoa将ip转为点分十进制的字符
std::string cli_info = inet_ntoa(endpoint.sin_addr);
cli_info += ":";
//to_string同样是将16为短整型的port转为字符串
cli_info += std::to_string(ntohs(endpoint.sin_port));
std::cout << "get a new link..."<< cli_info << std::endl;
service(sock);
}
}
~tcpServer()
{
close(lsock);
}
};
#endif
- tcpServer.cc
#include "tcpServer.hpp"
void Usage(std::string proc)
{
std::cout << "Usage: "<< std::endl;
std::cout<<'\t'<<proc<<" port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
tcpServer *tp = new tcpServer(atoi(argv[1]));
tp->initServer();
tp->start();
delete tp;
return 0;
}
<2> 客户端代码
- tcpClient.hpp
#ifndef __TCP_CLIENT_H__
#define __TCP_CLIENT_H__
#include<iostream>
#include<string>
#include<stdlib.h>
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
class tcpClient{
private:
std::string svr_ip; //服务器ip
int svr_port; //服务器端口号
int sock; //sock文件描述符
public:
//构造函数
tcpClient(std::string _ip="127.0.0.1",int _port = 9999)
:svr_ip(_ip),svr_port(_port)
{}
//初始化
void initClient()
{
//1.创建套接字
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
std::cerr << "sock error" << std::endl;
exit(2);
}
//2.客户端不需要绑定,因为应用较多,如果固定绑定了某一端口造成别的应用无法使用,交给系统自动绑定即可
struct sockaddr_in svr;
svr.sin_family = AF_INET;
svr.sin_port = htons(svr_port);
svr.sin_addr.s_addr = inet_addr(svr_ip.c_str());
if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) != 0)
{
std::cerr << "connect error" << std::endl;
}
}
void start()
{
char msg[64];
while(true)
{
std::cout << "Please Input Msg#" << std::endl;
fflush(stdout);
size_t s = read(0,msg,sizeof(msg)-1);
if(s > 0)
{
//注意这里的细节
//read会把键盘上的回车读入,因此少读一个字节
msg[s-1] = 0;
send(sock,msg,strlen(msg),0);
size_t ss = recv(sock, msg, sizeof(msg)-1,0);
if(ss > 0)
{
msg[ss] = 0;
std::cout << "server echo #" << msg << std::endl;
}
}
}
}
~tcpClient()
{
close(sock);
}
};
#endif
- tcpClient.cc
#include"tcpClient.hpp"
void Usage(std::string proc)
{
std::cout << "Usage: " << std::endl;
std::cout << '\t' << proc << " " << "ser_ip ser_port" << std::endl;
}
int main(int argc, char * argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
tcpClient *tc = new tcpClient(argv[1],atoi(argv[2]));
tc->initClient();
tc->start();
delete tc;
return 0;
}
<3> Makefile
FLAG =-std=c++11
.PHONY:all
all:tcpClient tcpServer
tcpClient:tcpClient.cc
g++ -o $@ $^ $(FLAG)
tcpServer:tcpServer.cc
g++ -o $@ $^ $(FLAG)
.PHONY:clean
clean:
rm -f tcpClient tcpServer
<4> GitHub源码
https://github.com/Kyrie-leon/Linux/tree/main/practice/Network/03-sockettcp