理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.

源ip地址就是发送端ip,目的ip地址就是接收端ip

思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出这个数据要给哪个程序进行解析,于是我们引出了端口号

认识端口号

端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

因此,套接字通信本质是进程间通信

理解 “端口号” 和 “进程ID”

pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?

答:进程id的作用是在系统的多个进程中标识某一个进程,而端口号则是在众多网络进程中标识某一个进程

比如10086,10086相当于ip地址,而我们转接的人工客服相当于一个服务器进程,员工编号是端口号,客服的身份证号是pid

另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号.

源端口号就是发送数据的主机进程端口号,目的端口号就是接收数据的主机进程端口号。即描述 “数据是谁发的, 要发给谁”;

认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

TCP会处理丢包了的情况,也就是会保证一定的效率

认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

与TCP不同,UDP只负责发送,丢包了它并不会处理,也就是不会保证发送数据的效率

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);      //把uint32_t类型从主机序转换到网络序
uint16_t htons(uint16_t hostshort);     //把uint16_t类型从主机序转换到网络序
uint32_t ntohl(uint32_t netlong);       //把uint32_t类型从网络序转换到主机序
uint16_t ntohs(uint16_t netshort);      //把uint16_t类型从网络序转换到主机序
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

socket编程接口

socket 常见API

// 创建 socket文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数:domain表示域,表明使用的某种协议;type是套接字类别;protocol表示所采用协议,一般设置为0,也就是系统默认的
domain:我们常用的是AF_INET选项;type:我们主要用的两种,一种是TCP协议的sock_STREAM,一种是UDP协议的sock_DGRAM
返回值:创建成功返回文件描述符,失败返回-1.由此看出套接字也是文件// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
参数:socket表示需要绑定的套接字文件描述符,address是sockaddr的地址,address_len表示address指向的结构体的大小// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

观察上面三种结构体,第一个成员变量都是一个16位地址,是为了方便引用第一个变量,从而判断它属于哪种结构体。而socket的API一般都是struct sockaddr*,在底层的实现又各不相同,这种原理实际上就是多态,通过一套接口完成多种通信。

Linux下一切皆文件也是利用了多态。

sockaddr 结构

struct sockaddr {unsigned short sa_family; /* address family, AF_xxx */char sa_data[14]; /* 14 bytes of protocol address */};

sockaddr_in 结构

struct sockaddr_in {short int sin_family; /* Address family */unsigned short int sin_port; /* Port number */struct in_addr sin_addr; /* Internet address */unsigned char sin_zero[8]; /* Same size as struct sockaddr */};

in_addr结构

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数

  • n_family:指代协议族,在socket编程中只能是AF_INET
  • sin_port:存储端口号(使用网络字节顺序)
  • sin_addr:存储IP地址,使用in_addr这个数据结构,也就是上面图片中的
  • sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。

虽然socket API的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主
要有三部分信息: 地址类型, 端口号, IP地址

简单的UDP网络程序

实现一个简单的英译汉的功能
备注: 代码中会用到 地址转换函数 .

UDP通用服务器和客户端

里面要用到的几个接口:

//1.
ssize_t recvfrom(int socket, void *restrict buffer, size_t length,int flags, struct sockaddr *restrict address,socklen_t *restrict address_len);
参数:socket:套接字文件描述符;restrict buffer:接收数据的缓冲区(内存块);length:预计接收的字节长度;flags:表示没有数据时是否阻塞; restrict address:输出型参数,指向(接收)发送者的信息(ip、port等);
restrict address_len:输入输出型参数,输入的是参数restrict address的长度,输出的是实际返回给restrict address的结构体的大小
返回值:实际接收到的数据的大小//2.
ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,socklen_t dest_len);
与recvfrom函数的参数类似,message表示保存发送信息的内存块,dest_addr保存接受者的信息,dest_len是dest_addr结构体的大小//3.
in_addr_t inet_addr(const char *cp);将字符串ip转换成in_addr_t,
什么是in_addr_t: typedef uint32_t in_addr_t;//32位的无符号整型//4.
in_addr转字符串的函数:
char *inet_ntoa(struct in_addr inaddr);

服务器端
udpServer.hpp:

#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;class udpServer{private:
string _ip;//ip地址int _port;//端口号int _sock;//套接字文件描述符public://构造udpServer(string ip = "127.0.0.1", int port = 8080):_ip(ip),_port(port){}//初始化服务器void initServer(){//1.创建套接字//2.将套接字与ip、端口号绑定_sock = socket(AF_INET, SOCK_DGRAM, 0);//套接字也是文件,所以_sock实际上也是一个文件描述符,或者说,它就是3cout << _sock << endl;sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);//将主机端口转换成网络端口local.sin_addr.s_addr = inet_addr(_ip.c_str());//因为bind的第二个参数是struct sockaddr*类型的,所以需要强转if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(1);}}// 启动服务器void start(){char msg[64];while (1)//因为服务器一旦启动就不会轻易停下,除了更新等情况,所以用死循环控制其一直处于启动状态{msg[0] = '\0';struct sockaddr_in end_point;//接收远端客户端的信息:ip、port等socklen_t len;//客户端信息结构体的的大小ssize_t s = recvfrom(_sock, msg, sizeof(msg) - 1, 0, (struct sockaddr*)&end_point, &len);//len是一个输入输出型参数if (s > 0){msg[s] = '\0';//打印接收到的数据cout << "client say# " << msg << endl;//服务器回应string echo_string(msg, msg + s);//string echo_string;echo_string += "[server receive]";//发送信息sendto(_sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&end_point, len);}} }//析构,没什么资源需要清理~udpServer(){}};

udpServer.cpp

#include"udpServer.hpp"int main()
{auto pus = new udpServer;pus->initServer();pus->start();delete pus;return 0;
}

客户端
udpClient.hpp

#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;class udpClient{private:
string _ip;//ip地址int _port;//端口号int _sock;//套接字文件描述符public://构造//客户端的_ip和_port是服务器的,因为他要发信息给服务器udpClient(string ip = "127.0.0.1", int port = 8080):_ip(ip),_port(port){}//初始化客户端void initClient(){//客户端不需要绑定//创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);}// 启动客户端void start(){//先发再收string msg;struct sockaddr_in peer;//保存接收者的信息:ip、port等peer.sin_family = AF_INET;peer.sin_port = htons(_port);//将主机端口转换成网路端口peer.sin_addr.s_addr = inet_addr(_ip.c_str());socklen_t len = sizeof(peer);//接收端信息结构体的的大小while (1){cout << "Please Enter:";cin >> msg;//发送信息sendto(_sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, len);//接收信息char echo[128];ssize_t s = recvfrom(_sock, echo, sizeof(echo) - 1, 0, nullptr, nullptr);//不关心发送者的信息if (s > 0){echo[s] = '\0';//打印接收到的数据cout <<  echo << endl;}} }//析构,没什么资源需要清理~udpClient(){}};

udpClient.cpp

#include"udpClient.hpp"int main()
{auto uct = new udpClient;uct->initClient();uct->start();delete uct;return 0;
}

效果:

为什么服务器需要绑定端口号,而客户端无须绑定?

服务器是要一直运行的,除非更新等情况才会停止运行,所以要固定一个ip地址和端口号,以便客户端访问,而客户端不用一直运行,所以使用系统分配的合适一点,因为系统清楚端口号的分配。如果使用自己定义的,可能这个ip或端口号正在被使用或者正要被使用,这样就会导致绑定失败,从而客户端运行不起来。

服务器与客户端对话2.0

主要改动是以命令行参数的形式传ip、端口号;server端不用固定ip,这样每一个客户端只要端口一致就可以和它通信;展示客户端信息(ip、port)

udpServer.hpp(ip地址设置成INADDR_ANY,展示客户端信息)

#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;class udpServer{private://string _ip;//ip地址int _port;//端口号int _sock;//套接字文件描述符public://构造udpServer(int port = 8080):_port(port){}//初始化服务器void initServer(){//1.创建套接字//2.将套接字与ip、端口号绑定_sock = socket(AF_INET, SOCK_DGRAM, 0);//套接字也是文件,所以_sock实际上也是一个文件描述符,或者说,它就是3cout << _sock << endl;sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);//将主机端口转换成网络端口local.sin_addr.s_addr = INADDR_ANY;//任意绑定//因为bind的第二个参数是struct sockaddr*类型的,所以需要强转if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(1);}}// 启动服务器void start(){char msg[64];while (1)//因为服务器一旦启动就不会轻易停下,除了更新等情况,所以用死循环控制其一直处于启动状态{msg[0] = '\0';struct sockaddr_in end_point;//接收远端客户端的信息:ip、port等socklen_t len;//客户端信息结构体的的大小ssize_t s = recvfrom(_sock, msg, sizeof(msg) - 1, 0, (struct sockaddr*)&end_point, &len);//len是一个输入输出型参数if (s > 0){//获取客户端的ip和portchar buf[16];//端口号,整型转成点分十进制sprintf(buf, "%d", ntohs(end_point.sin_port));string cli = inet_ntoa(end_point.sin_addr);//ip地址cli+=":";cli+=buf;msg[s] = '\0';//打印接收到的数据cout << cli << "# " << msg << endl;//服务器回应string echo_string(msg, msg + s);//string echo_string;echo_string += "[server receive]";//发送信息sendto(_sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&end_point, len);}} }//析构,没什么资源需要清理~udpServer(){close(_sock);}};

为什么ip地址是127.0.0.1?
127.0.0.1代表本地环回,通常用来进行网络通信代码的本地测试。一般跑通了,就说明本地环境以及代码基本没有大问题。

服务器的ip赋值成INADDR_ANY:任意绑定,服务器收到任意ip的信息都可以接收,不会只限定一个ip地址。

udpClient.hpp(不变)

#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;class udpClient{private:
string _ip;//ip地址int _port;//端口号int _sock;//套接字文件描述符public://构造//客户端的_ip和_port是服务器的,因为他要发信息给服务器udpClient(string ip = "127.0.0.1", int port = 8080):_ip(ip),_port(port){}//初始化客户端void initClient(){//客户端不需要绑定//创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);}// 启动客户端void start(){//先发再收string msg;struct sockaddr_in peer;//保存接收者的信息:ip、port等peer.sin_family = AF_INET;peer.sin_port = htons(_port);//将主机端口转换成网路端口peer.sin_addr.s_addr = inet_addr(_ip.c_str());socklen_t len = sizeof(peer);//接收端信息结构体的的大小while (1){cout << "Please Enter:";cin >> msg;//发送信息sendto(_sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, len);//接收信息char echo[128];ssize_t s = recvfrom(_sock, echo, sizeof(echo) - 1, 0, nullptr, nullptr);//不关心发送者的信息if (s > 0){echo[s] = '\0';//打印接收到的数据cout <<  echo << endl;}} }//析构,没什么资源需要清理~udpClient(){close(_sock);}};

udpServer.cpp

#include"udpServer.hpp"void manual(string cmd)
{cout << "manual " << cmd << " port" << endl;
}//使用命令行传参的形式传递port
int main(int argc, char*argv[])
{//如果命令格式不符合要求,则退出if (argc != 2){manual(argv[0]);exit(1);}auto pus = new udpServer(atoi(argv[1]));//端口号给服务器初始化pus->initServer();pus->start();delete pus;return 0;
}

udpClient.cpp

#include"udpClient.hpp"void manual(string cmd)
{cout << "manual " << cmd << " ip" << "port" << endl;
}int main(int argc, char*argv[])
{//如果命令格式不符合要求,则退出if (argc != 3){manual(argv[0]);exit(1);}auto uct = new udpClient(argv[1], atoi(argv[2]);uct->initClient();uct->start();delete uct;return 0;
}

实现英译汉服务器和客户端

服务器端
udpServer.hpp

添加一个map成员变量存储英汉互译的内容,服务器收到客户端的翻译请求,在map中查找该单词的翻译,找到了返回翻译,找不到就返回undefined。

#include<iostream>
#include<string>
#include<map>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;class udpServer{private://string _ip;//ip地址map<string, string> dic;int _port;//端口号int _sock;//套接字文件描述符public://构造udpServer(int port = 8080):_port(port){//放入几个简单的翻译dic.insert(make_pair("apple", "苹果,一种水果"));dic.insert(make_pair("banana", "香蕉,一种水果"));dic.insert(make_pair("student", "学生"));dic.insert(make_pair("life", "生活"));}//初始化服务器void initServer(){//1.创建套接字//2.将套接字与ip、端口号绑定_sock = socket(AF_INET, SOCK_DGRAM, 0);//套接字也是文件,所以_sock实际上也是一个文件描述符,或者说,它就是3cout << _sock << endl;sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);//将主机端口转换成网络端口local.sin_addr.s_addr = INADDR_ANY;//任意绑定//因为bind的第二个参数是struct sockaddr*类型的,所以需要强转if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(1);}}// 启动服务器void start(){char msg[64];while (1)//因为服务器一旦启动就不会轻易停下,除了更新等情况,所以用死循环控制其一直处于启动状态{msg[0] = '\0';struct sockaddr_in end_point;//接收远端客户端的信息:ip、port等socklen_t len;//客户端信息结构体的的大小ssize_t s = recvfrom(_sock, msg, sizeof(msg) - 1, 0, (struct sockaddr*)&end_point, &len);//len是一个输入输出型参数if (s > 0){//获取客户端的ip和portchar buf[16];//端口号,整型转成点分十进制sprintf(buf, "%d", ntohs(end_point.sin_port));string cli = inet_ntoa(end_point.sin_addr);//ip地址cli+=":";cli+=buf;msg[s] = '\0';auto pos = dic.find(msg);string echo = "undefined";if (pos != dic.end())//找到了{echo = dic[msg];}//打印接收到的数据cout << cli << "# " << msg << endl;//服务器回应string echo_string(echo);//string echo_string;//发送信息sendto(_sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&end_point, len);}} }//析构,没什么资源需要清理~udpServer(){}};

客户端
udpClient.hpp

发送要翻译的单词

#include<iostream>
#include<string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;class udpClient{private:
string _ip;//ip地址int _port;//端口号int _sock;//套接字文件描述符public://构造//客户端的_ip和_port是服务器的,因为他要发信息给服务器udpClient(string ip = "127.0.0.1", int port = 8080):_ip(ip),_port(port){}//初始化客户端void initClient(){//客户端不需要绑定//创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);}// 启动客户端void start(){//先发再收string msg;struct sockaddr_in peer;//保存接收者的信息:ip、port等peer.sin_family = AF_INET;peer.sin_port = htons(_port);//将主机端口转换成网路端口peer.sin_addr.s_addr = inet_addr(_ip.c_str());socklen_t len = sizeof(peer);//接收端信息结构体的的大小while (1)//因为服务器一旦启动就不会轻易停下,除了更新等情况,所以用死循环控制其一直处于启动状态{cout << "Please Enter:";cin >> msg;//发送信息sendto(_sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, len);struct sockaddr_in end_point;socklen_t len = sizeof(end_point);//接收信息char echo[128];ssize_t s = recvfrom(_sock, echo, sizeof(echo) - 1, 0, (struct sockaddr*)&end_point, &len);//不关心发送者的信息if (s > 0){echo[s] = '\0';//打印接收到的数据cout << "translation: " <<  echo << endl;}} }//析构,没什么资源需要清理~udpClient(){}};

udpClient.cpp和udpServer.cpp和之前的一样。

效果:

地址转换函数

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串转in_addr的函数:

#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);in_addr_t inet_addr(const char *cp);
int inet_pton(int af, const char *src, void *dst);

in_addr转字符串的函数:

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr

关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码

运行结果如下

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

多线程调用inet_ntoa代码示例如下

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>void* Func1(void* p)
{struct sockaddr_in* addr = (struct sockaddr_in*)p;while (1) {char* ptr = inet_ntoa(addr->sin_addr);printf("addr1: %s\n", ptr);}return NULL;
}void* Func2(void* p)
{struct sockaddr_in* addr = (struct sockaddr_in*)p;while (1) {char* ptr = inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);}return NULL;
}int main()
{pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;//ip1addr2.sin_addr.s_addr = 0xffffffff;//ip2pthread_create(&tid1, NULL, Func1, &addr1);pthread_t tid2 = 0;pthread_create(&tid2, NULL, Func2, &addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}

测试结果:
并没有出现异常情况

简单的TCP网络程序

和刚才UDP类似. 实现一个简单的英译汉的功能
TCP socket API 详解
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。
socket() :

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
  • 应用程序可以像读写文件一样用read/write在网络上收发数据;
  • 如果socket()调用出错则返回-1;
  • 对于IPv4, family参数指定为AF_INET;
  • 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
  • protocol参数的介绍从略,指定为0即可

bind():

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
  • bind()成功返回0,失败返回-1。
  • bind()的作用是将参数socket和address绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听address所描述的地址和端口号;
  • 前面讲过,struct sockaddr *是一个通用指针类型,address参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数len指定结构体的长度;

我们的程序中对myaddr参数是这样初始化的:

  1. 将整个结构体清零;
  2. 设置地址类型为AF_INET;
  3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
  4. 端口号为SERV_PORT, 我们定义为8080

listen():

  • listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5)
  • listen()成功返回0,失败返回-1

accept():

  • 三次握手完成后, 服务器调用accept()接受连接;

  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;

  • addr是一个输出型参数,accept()返回时传出客户端的地址和端口号;

  • 如果给addr参数传NULL,表示不关心客户端的信息;

  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)

    • accept的返回值是什么呢?

我们看到,如果accept连接成功,则返回一个非负的套接字文件描述符,可是我们不是已经有了一个socket吗,为啥还要再返回一个?
实际上,这两个套接字的功能是不一样的,我们传入的套接字只负责连接服务器与客户端,而返回的套接字则负责两者之间的通信。这就有点像我们入住酒店时,前台负责帮我们办理入住,而服务人员负责我们的吃喝清洁等服务,两者的作用不一样,但都是在为我们服务。

connect

  • 客户端需要调用connect()连接服务器;
  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1;

tcp发送数据和接收数据的接口:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:sockfd:进行通信的套接字
buf:发送的数据所在缓冲区
len:期望发送的数据长度(单位:字节)
flags:一般设置为0返回值:实际发送的数据长度ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:sockfd:进行通信的套接字
buf:接收数据的缓冲区
len:期望接收数据的长度 (单位:字节)
flag:一般设置为0返回值:实际接收的数据长度

只实现了服务器

tcpServer.hpp

#include<iostream>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#define BACKLOG 5using namespace std;class tcpServer{private:int _port;int _lsock;//监听套接字public://构造tcpServer(int port = 8080):_port(port){}void initServer(){//1.创建套接字//2.绑定//3.监听,tcp套接字需要进行监听_lsock = socket(AF_INET, SOCK_STREAM, 0);if (_lsock < 0){cerr << "socket error!\n" << endl;exit(1);}//绑定struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_lsock,(struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(2);}//监听if (listen(_lsock, BACKLOG) < 0){cerr << "listen error!\n" << endl;exit(3);}}void service(int sock){while (1){char buf[64];//接收信息size_t s = recv(sock, buf, sizeof(buf) - 1 , 0);if (s > 0){buf[s] = '\0';cout << "client# " << buf << endl;//服务器回应string echo(buf);echo += "[server echo]";send(sock, echo.c_str(), echo.size(), 0);}if (s == 0)//客户端退出{//服务器端也要终止对当前客户端的服务,然后返回,等待下一个客户端的连接(accept)cout << "client quit..." << endl;close(sock);break;}}}void start(){while (1){//服务器与客户端连接struct sockaddr_in end_point;socklen_t len = sizeof(end_point);int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);if (sock < 0){cerr << "accept error!\n" << endl;exit(4);}  cout << "get a link..." << endl;//通信service(sock);}}~tcpServer(){close(_lsock);}
};

tcpServer.cpp

#include"tcpServer.hpp"void usage(char* cmd)
{cout << "usage:" << endl;cout << "\t" << cmd << " port" << endl;
}int main(int argc, char*argv[])
{if (argc != 2){usage(argv[0]);exit(1);}auto tsr = new tcpServer(atoi(argv[1]));tsr->initServer();tsr->start();delete tsr;return 0;
}

我们暂且还未实现client的功能,就使用telnet登录,远端连接服务器。
以下是连接时我们查看当前的网络状态:

为什么会有两条链接呢?因为客户端和服务器在一台机器上,如果是在两台机器上,则服务器显示的是上面那条链接,客户端是下面那条

telnet:输入 “^]”,进行通信

效果:

TCP服务器与客户端通信(单进程版本)

tcpServer.hpp

#pragma once
#include<iostream>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#define BACKLOG 5using namespace std;class tcpServer{private:int _port;int _lsock;//监听套接字public://构造tcpServer(int port = 8080):_port(port){}void initServer(){//1.创建套接字//2.绑定//3.监听,tcp套接字需要进行监听_lsock = socket(AF_INET, SOCK_STREAM, 0);if (_lsock < 0){cerr << "socket error!\n" << endl;exit(1);}//绑定struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_lsock,(struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(2);}//监听if (listen(_lsock, BACKLOG) < 0){cerr << "listen error!\n" << endl;exit(3);}}void service(int sock){while (1){char buf[64];//接收信息size_t s = recv(sock, buf, sizeof(buf) - 1 , 0);if (s > 0){buf[s] = '\0';cout << "client# " << buf << endl;//服务器回应string echo(buf);echo += "[server echo]";send(sock, echo.c_str(), echo.size(), 0);}}}void start(){while (1){//服务器与客户端连接struct sockaddr_in end_point;socklen_t len = sizeof(end_point);int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);if (sock < 0){cerr << "accept error!\n" << endl;exit(4);}cout << "get a link" << endl;//通信service(sock);}}~tcpServer(){close(_lsock);}
};

tcpClient.hpp

#include<iostream>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>using namespace std;class tcpClient{private:string _ip;int _port;int _sock;public:tcpClient(string ip = "127.0.0.1", int port = 8080):_ip(ip),_port(port){}void initClient(){//1.创建套接字//2.连接客户端_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){cerr << "socket error!" << endl;exit(1);}//connect//服务器信息sockaddr_in svr;svr.sin_family = AF_INET;svr.sin_port = htons(_port);svr.sin_addr.s_addr = inet_addr(_ip.c_str());if (connect(_sock, (struct sockaddr*)&svr, sizeof(svr)) != 0){cerr << "connect error!" << endl;exit(2);}}void start(){char msg[64];while(1){//先发再收ssize_t s = read(0, msg, sizeof(msg) - 1);//read会把我们按下的回车也读到msg中,所以msg[s - 1]就是\nif (s > 0){msg[s - 1] = '\0';send(_sock, msg, strlen(msg), 0);//接收服务器的反馈ssize_t ss = recv(_sock, msg, sizeof(msg)-1, 0);if (ss > 0){msg[ss] = '\0';cout << msg << endl;}}}}~tcpClient(){close(_sock);}
};

英译汉服务器与客户端

服务器

server.hpp

#include<iostream>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <map>
#define BACKLOG 5using namespace std;class tcpServer{private:int _port;int _lsock;//监听套接字map<string, string> dict;public://构造tcpServer(int port = 8080):_port(port){dict.insert(make_pair("apple", "苹果"));dict.insert(make_pair("banana", "香蕉"));dict.insert(make_pair("camera", "相机"));dict.insert(make_pair("study", "学习"));}void initServer(){//1.创建套接字//2.绑定//3.监听,tcp套接字需要进行监听_lsock = socket(AF_INET, SOCK_STREAM, 0);if (_lsock < 0){cerr << "socket error!\n" << endl;exit(1);}//绑定struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_lsock,(struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(2);}//监听if (listen(_lsock, BACKLOG) < 0){cerr << "listen error!\n" << endl;exit(3);}}void service(int sock){while (1){char buf[64];//接收信息size_t s = recv(sock, buf, sizeof(buf) - 1 , 0);if (s > 0){buf[s] = '\0';string word(buf);//服务器翻译string echo("undefined");//找到了就是翻译结果,没找到就是未定义auto pos = dict.find(word);if (pos != dict.end())//找到了{echo = pos->second;}cout << "translation: " << echo << endl; send(sock, echo.c_str(), echo.size(), 0);}else if (s == 0){cout << "client exit" << endl;break;}else{cout << "recv error !" << endl;break;}}}void start(){while (1){//服务器与客户端连接struct sockaddr_in end_point;socklen_t len = sizeof(end_point);int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);if (sock < 0){cerr << "accept error!\n" << endl;exit(4);}cout << "get a link" << endl;//通信service(sock);}}~tcpServer(){close(_lsock);}
};

server.cpp

#include"tcpServer.hpp"void usage(char* cmd)
{cout << "usage:" << endl;cout << "\t" << cmd << " port" << endl;
}int main(int argc, char*argv[])
{if (argc != 2){usage(argv[0]);exit(1);}auto tsr = new tcpServer(atoi(argv[1]));tsr->initServer();tsr->start();delete tsr;return 0;
}

客户端

client.hpp

#include<iostream>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>using namespace std;class tcpClient{private:string _ip;int _port;int _sock;public:tcpClient(string ip = "127.0.0.1", int port = 8080):_ip(ip),_port(port){}void initClient(){//1.创建套接字//2.连接客户端_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){cerr << "socket error!" << endl;exit(1);}//connect//服务器信息sockaddr_in svr;svr.sin_family = AF_INET;svr.sin_port = htons(_port);svr.sin_addr.s_addr = inet_addr(_ip.c_str());if (connect(_sock, (struct sockaddr*)&svr, sizeof(svr)) != 0){cerr << "connect error!" << endl;exit(2);}}void start(){char msg[64];while(1){//先发再收ssize_t s = read(0, msg, sizeof(msg) - 1);//read会把我们按下的回车也读到msg中,所以msg[s - 1]就是\nif (s > 0){msg[s - 1] = '\0';send(_sock, msg, strlen(msg), 0);//接收服务器的反馈ssize_t ss = recv(_sock, msg, sizeof(msg)-1, 0);if (ss > 0){msg[ss] = '\0';cout << msg << endl;}}else if(s == 0){cout << "read end !" << endl;break;}else {cout << "read error !" << endl;break;}}}~tcpClient(){close(_sock);}
};

client.cpp

#include"tcpClient.hpp"void usage(char* cmd)
{cout << "usage:" << endl;cout << "\t" << cmd << " ip port" << endl;
}int main(int argc, char*argv[])
{if (argc != 3){usage(argv[0]);exit(1);}auto tcl = new tcpClient(argv[1], atoi(argv[2]));tcl->initClient();tcl->start();delete tcl;return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配.
注意:

  • 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动多个客户端, 就会出现端口号被占用导致不能正确建立连接;
  • 服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦

测试多个连接的情况

再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信.
分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试recv, 没有继续调用到accecpt, 导致不能接受新的请求.

我们当前的这个TCP, 只能处理一个连接, 这是不科学的

简单的TCP网络程序(多进程版本)

通过每个请求, 创建子进程的方式来支持多连接;
tcpServer.hpp

#include<iostream>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<signal.h>
#define BACKLOG 5using namespace std;class tcpServer{private:int _port;int _lsock;//监听套接字public://构造tcpServer(int port = 8080):_port(port){}void initServer(){//将子进程退出发送的SIGCHLED信号忽略signal(SIGCHLD, SIG_IGN);//1.创建套接字//2.绑定//3.监听,tcp套接字需要进行监听_lsock = socket(AF_INET, SOCK_STREAM, 0);if (_lsock < 0){cerr << "socket error!\n" << endl;exit(1);}//绑定struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_lsock,(struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(2);}//监听if (listen(_lsock, BACKLOG) < 0){cerr << "listen error!\n" << endl;exit(3);}}void service(int sock){while (1){char buf[64];//接收信息size_t s = recv(sock, buf, sizeof(buf) - 1 , 0);if (s > 0){buf[s] = '\0';cout << "client# " << buf << endl;//服务器回应string echo(buf);echo += "[server echo]";send(sock, echo.c_str(), echo.size(), 0);}}}void start(){while (1){//服务器与客户端连接struct sockaddr_in end_point;socklen_t len = sizeof(end_point);int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);if (sock < 0){cerr << "accept error!\n" << endl;exit(4);}string cli_info = inet_ntoa(end_point.sin_addr);cli_info += ":";cli_info += to_string(ntohs(end_point.sin_port));cout << "get a link: " << cli_info  << endl;pid_t id = fork();//创建子进程if (id == 0)//子进程{close(_lsock);//不关心_lsock,所以可以关掉service(sock);exit(0);}//子进程的退出需要父进程等待然后回收,但这样会影响父进程去连接其他客户端。//子进程退出时会发送SIGCHLD信号给父进程,让父进程来回收他,而我们让父进程忽略该信号(init处),让系统来回收子进程,就不会对父进程的工作有影响。close(sock);//父进程用不着sock,所以也可以关掉,空出文件描述符,否则可用文件描述符会越来越少//这样的话,每一个server进程都是使用同一个sock}}~tcpServer(){close(_lsock);}
};

简单的TCP网络程序(多线程版本)

通过每个请求, 创建一个线程的方式来支持多连接;
tcp_thread_server.hpp (客户端代码和多进程相同)

#include<iostream>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<signal.h>
#include<pthread.h>#define BACKLOG 5using namespace std;class tcpServer{private:int _port;int _lsock;//监听套接字public://构造tcpServer(int port = 8080):_port(port){}void initServer(){//将子进程退出发送的SIGCHLED信号忽略signal(SIGCHLD, SIG_IGN);//1.创建套接字//2.绑定//3.监听,tcp套接字需要进行监听_lsock = socket(AF_INET, SOCK_STREAM, 0);if (_lsock < 0){cerr << "socket error!\n" << endl;exit(1);}//绑定struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_lsock,(struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(2);}//监听if (listen(_lsock, BACKLOG) < 0){cerr << "listen error!\n" << endl;exit(3);}}static void service(int sock){while (1){char buf[64];//接收信息size_t s = recv(sock, buf, sizeof(buf) - 1 , 0);if (s > 0){buf[s] = '\0';cout << "client# " << buf << endl;//服务器回应string echo(buf);echo += "[server echo]";send(sock, echo.c_str(), echo.size(), 0);}else if (s == 0){cout << "client exit..." << endl;break;}else{cout << "recv error !" << endl;break;}}}static void*serverRoutine(void* arg){//多线程不能关闭sock,因为它们共用文件描述符表//线程分离,防止主线程等待pthread_detach(pthread_self());cout << "create a new thread for IO" << endl;int sock = *(int*)arg;service(sock);delete (int*)arg;}void start(){while (1){//服务器与客户端连接struct sockaddr_in end_point;socklen_t len = sizeof(end_point);int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);if (sock < 0){cerr << "accept error!\n" << endl;exit(4);}string cli_info = inet_ntoa(end_point.sin_addr);cli_info += ":";cli_info += to_string(ntohs(end_point.sin_port));cout << "get a link: " << cli_info  << endl;pthread_t tid;int*p = new int(sock);pthread_create(&tid, nullptr, serverRoutine, (void*)p);//为什么不传sock?  如果线程创建慢了,主线程又去连接了新的sock,此时创建还未完毕,sock就改变了。/* pid_t id = fork();//创建子进程if (id == 0)//子进程{close(_lsock);//不关心_lsock,所以可以关掉service(sock);exit(0);}//子进程的退出需要父进程等待然后回收,但这样会影响父进程去连接其他客户端。//子进程退出时会发送SIGCHLD信号给父进程,让父进程来回收他,而我们让父进程忽略该信号(init处),让系统来回收子进程,就不会对父进程的工作有影响。close(sock);//父进程用不着sock,所以也可以关掉,空出文件描述符,否则可用文件描述符会越来越少//这样的话,每一个server进程都是使用同一个sock*/}}~tcpServer(){close(_lsock);}
};

测试:

总结:

  1. 单进程:不可使用

  2. 多进程版本:健壮性好,但比较吃资源,效率低下

  3. 多线程版本:健壮性不强,较吃资源,效率相对较高
    如果存在大量客户端,系统会存在大量的执行流,线程间切换有可能成为效率低下的重要原因

    虽然多线程效率较高,但每次都要等客户端发送请求了才创建线程,我们可以使用线程池提前创建好线程,进一步提高效率

简单的TCP网络程序(线程池版本)

服务器端:

server.cpp

#include"ThreadPool.hpp"#include<iostream>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<signal.h>
#include<pthread.h>#define BACKLOG 5using namespace std;class tcpServer{private:int _port;int _lsock;//监听套接字ThreadPool* tp;public://构造tcpServer(int port = 8080):_port(port){}void initServer(){//将子进程退出发送的SIGCHLED信号忽略signal(SIGCHLD, SIG_IGN);//1.创建套接字//2.绑定//3.监听,tcp套接字需要进行监听_lsock = socket(AF_INET, SOCK_STREAM, 0);if (_lsock < 0){cerr << "socket error!\n" << endl;exit(1);}//绑定struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_lsock,(struct sockaddr*)&local, sizeof(local)) < 0){cerr << "bind error!\n" << endl;exit(2);}//监听if (listen(_lsock, BACKLOG) < 0){cerr << "listen error!\n" << endl;exit(3);}tp = new(ThreadPool);tp->ThreadInit();}static void service(int sock){while (1){char buf[64];//接收信息size_t s = recv(sock, buf, sizeof(buf) - 1 , 0);if (s > 0){buf[s] = '\0';cout << "client# " << buf << endl;//服务器回应string echo(buf);echo += "[server echo]";send(sock, echo.c_str(), echo.size(), 0);}else if (s == 0){cout << "client exit..." << endl;break;}else{cout << "recv error !" << endl;break;}}}static void*serverRoutine(void* arg){//多线程不能关闭sock,因为它们共用文件描述符表//线程分离,防止主线程等待pthread_detach(pthread_self());cout << "create a new thread for IO" << endl;int sock = *(int*)arg;service(sock);delete (int*)arg;}void start(){while (1){//服务器与客户端连接struct sockaddr_in end_point;socklen_t len = sizeof(end_point);int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);if (sock < 0){cerr << "accept error!\n" << endl;exit(4);}string cli_info = inet_ntoa(end_point.sin_addr);cli_info += ":";cli_info += to_string(ntohs(end_point.sin_port));cout << "get a link: " << cli_info  << endl;Task*t = new Task(sock);tp->Put(*t);/*pthread_t tid;int*p = new int(sock);pthread_create(&tid, nullptr, serverRoutine, (void*)p);//为什么不传sock?  如果线程创建慢了,主线程又去连接了新的sock,此时创建还未完毕,sock就改变了。*//* pid_t id = fork();//创建子进程if (id == 0)//子进程{close(_lsock);//不关心_lsock,所以可以关掉service(sock);exit(0);}//子进程的退出需要父进程等待然后回收,但这样会影响父进程去连接其他客户端。//子进程退出时会发送SIGCHLD信号给父进程,让父进程来回收他,而我们让父进程忽略该信号(init处),让系统来回收子进程,就不会对父进程的工作有影响。close(sock);//父进程用不着sock,所以也可以关掉,空出文件描述符,否则可用文件描述符会越来越少//这样的话,每一个server进程都是使用同一个sock*/}}~tcpServer(){close(_lsock);}
};

客户端:

client.cpp

#include<iostream>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>using namespace std;class tcpClient{private:string _ip;int _port;int _sock;public:tcpClient(string ip = "127.0.0.1", int port = 8080):_ip(ip),_port(port){}void initClient(){//1.创建套接字//2.连接客户端_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){cerr << "socket error!" << endl;exit(1);}//connect//服务器信息sockaddr_in svr;svr.sin_family = AF_INET;svr.sin_port = htons(_port);svr.sin_addr.s_addr = inet_addr(_ip.c_str());if (connect(_sock, (struct sockaddr*)&svr, sizeof(svr)) != 0){cerr << "connect error!" << endl;exit(2);}}void start(){char msg[64];cout << "Plead Enter: " ;fflush(stdout);//先发再收ssize_t s = read(0, msg, sizeof(msg) - 1);//read会把我们按下的回车也读到msg中,所以msg[s - 1]就是\nif (msg[0] == '\n'){cout << "\nPlease Enter Again !\n";} if (s > 0){msg[s - 1] = '\0';send(_sock, msg, strlen(msg), 0);//接收服务器的反馈ssize_t ss = recv(_sock, msg, sizeof(msg)-1, 0);if (ss > 0){msg[ss] = '\0';cout << "translation: " <<msg << endl;}}}~tcpClient(){close(_sock);}
};

初识网络及socket编程基础相关推荐

  1. Java从零开始学四十五(Socket编程基础)

    一.网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输. 在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可 ...

  2. 【转】Java Socket编程基础及深入讲解

    原文:https://www.cnblogs.com/yiwangzhibujian/p/7107785.html#q2.3.3 Socket是Java网络编程的基础,了解还是有好处的, 这篇文章主要 ...

  3. java socket 重连复用_Java Socket编程基础及深入讲解(示例代码)

    Socket是Java网络编程的基础,了解还是有好处的, 这篇文章主要讲解Socket的基础编程.Socket用在哪呢,主要用在进程间,网络间通信.本篇比较长,特别做了个目录: 一.Socket通信基 ...

  4. [linux] Linux网络之Socket编程入门

    目录 1. 前言 2. 网络基础 2.1 协议 2.1.1 TCP和UDP协议 2.2 网络的层状结构 2.3 一台主机向另一台主机的发送数据的流向 2.4 IP和MAC地址 2.5 端口 2.6 网 ...

  5. 你得学会并且学得会的Socket编程基础知识

    这一篇文章,我将图文并茂地介绍Socket编程的基础知识,我相信,如果你按照步骤做完实验,一定可以对Socket编程有更好地理解. 本文源代码,可以通过这里下载 http://files.cnblog ...

  6. Linux下基于C/C++的Socket编程基础

    什么是Socket Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序.要学Internet上的TCP/IP网络编程, ...

  7. Python学习笔记——基础篇【第七周】———FTP作业(面向对象编程进阶 Socket编程基础)...

    FTP作业 本节内容: 面向对象高级语法部分 Socket开发基础 作业:开发一个支持多用户在线的FTP程序 面向对象高级语法部分 参考:http://www.cnblogs.com/wupeiqi/ ...

  8. 你得学会并且学得会的Socket编程基础知识(转)

    这一篇文章,我将图文并茂地介绍Socket编程的基础知识,我相信,如果你按照步骤做完实验,一定可以对Socket编程有更好地理解. 本文源代码,可以通过这里下载 http://files.cnblog ...

  9. socket编程基础知识

    一.预备知识 1.理解源IP和目的IP 因特网上的每台计算机都有一个唯一的IP地址,如果一台主机上的数据要通过网络传输到另一台主机,那么对端主机的IP地址就应该作为该数据传输时的目的IP地址.但仅仅知 ...

最新文章

  1. Spring data redis乱码问题
  2. 高速串行总线设计基础(四)眼图的形成原理
  3. 客户关系管理系统-CRM源码
  4. 玩转oracle 11g(33):无监听程序
  5. 关于概率算法的问题,不知道逻辑错在哪里,求debug
  6. MySQL 输入输出 XML
  7. R40 gpio 寄存器地址操作【原创】
  8. CentOS7--IP配置与网络问题排查
  9. 蓝绿部署、滚动部署、灰度发布、金丝雀发布-概念介绍---应用部署001
  10. Ubuntu18.04及以上设备安装CUDA,CUDNN,Anaconda亲测方法
  11. vue zxing 实现一维码、二维码扫描可移动端h5使用代码亲测可用
  12. spss需要计算机代码,SPSS编程操作入门
  13. linux a卡怎么切换n卡,手把手教您win10系统a卡切换独显的具体办法
  14. RMAN-06817: Pluggable Database CHARLESPDB cannot be backed up in NOARCHIVELOG mode.
  15. 【VUE】vue3学习笔记(异步组件,包含defineAsyncComponent、Suspense的使用)
  16. flink连接kafka报错
  17. 01旭锋集团运营平台v2项目概述
  18. 浅析swift optional
  19. linux小红帽实验心得,《小红帽》读后感读书心得400字五篇
  20. iOS icon图标尺寸

热门文章

  1. 众至上网行为管理,管控内网行为,提升安全水平
  2. 知道服务器地址上传网页文件,修改的网页怎么上传到服务器地址
  3. 东方财富函数 量化交易 第001记录 量价研究的开始
  4. JSP —— Servlet 单实例多线程模式
  5. 嵌入式学习指南(必备)
  6. PostgreSQL 随读笔记-事务上
  7. 2018信用卡你不得不知的6大新规
  8. 什么是QC080000有害物质过程控制管理体系认证辅导
  9. Mybatis的复习(一)
  10. 鸟哥私房菜linux基础学习笔记 4