目录
本节目标:
-
认识IP地址,端口号,网络字节序等网络编程中的基本概念。
-
学习socket、api的基本用法。
-
能够实现一个简单的udp客户端/服务器。
-
能够实现一个简单的tcp客户端/服务器(单连接版本,多进程版本,多线程版本。)
-
理解tcp服务器建立连接,发送数据,断开连接的流程。
-
接口都是系统调用接口,程序都是用户级程序。自定义协议,所有接口都是传输层接口。 应用层接口,使用传输层接口。
IP地址和端口号
理解源IP地址和目的IP地址
- 在IP数据包头部中,有2个IP地址。分别叫做源IP地址和目的IP地址。
- IP在公网标识唯一的IP地址。
- 目的IP是为了找到目标主机的位置。
- 源IP是为了让远端服务器找到自己。
端口号
- 一台主机发送数据给另外一台主机,不光需要知道该主机的位置,还需要知道给这台主机中的哪个进程发送数据。而端口号就是标识一台主机中的网络进程所用的。所以网络间的数据传输,实际上就是进程间的数据传输。
- IP标定全公网内唯一的一台主机。
- 端口号(port)标定特定一台主机上的唯一一个进程。
- IP + 端口号 : 全网唯一的进程。
- 套接字就是IP+端口号。
- 套接字可以跨网络通信。
- 套接字的本质就是进程间通信,进程间通信需要看到公共的资源,socket通信看到的公共资源是计算机网络。
端口号和进程ID
- 进程id是每个进程都需要的,而端口号是网络进程才需要的。
- 一个进程可以有多个端口号,一个端口号只能绑定一个进程。
- 一个进程可以绑定多个端口号,而一个端口号只能绑定一个进程。
源端口号和目的端口号
认识TCP, UDP
TCP
- 传输层协议。
- 有连接,发送数据时需要建立连接。
- 可靠传输:数据传输过程中出现的一系列问题,TCP都有对应的措施,这也导致它非常复杂。
- 面向字节流:将应用程序交付下来的数据块看成一连串的无结构的字节流进行传输,TCP并不知道传输的字节流的含义。
UDP
- 传输层协议。
- 无连接,发送数据时不需要建立连接。
- 不可靠传输,只管发送数据,至于发送后出了什么问题,一概不管。这让UDP比起TCP来说很简单。
- 面向数据报:发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。
网络字节序
1, 什么是网络字节序?
数据存储在计算机中有不同的方法,如大端存储和小端存储。大端存储就是按照字节数,高位存在低地址,低位存放在高地址。而小端存储就是按字节,低位存放在低地址,高位存放在高地址。网络中的数据流也有大小端之分,那么如何定义网络中数据流地址呢(小端机和大端机传过来的数据不一致,如何分辨)?
- 发送端主机按照地址的高低,先发送低地址的数据,再发送高地址的数据。
- 接收端由低到高的接收数据到缓冲区。
- TCP/IP 协议中规定,网络中的数据都是大端的。
- 如果发送端是小端机,那么发送前需要将数据转换成大端再发送到网络上;否则,直接发送。
socket编程接口
socket常见API
- socket, bind, listen, accept, connect,
- 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)。
- domain:使用的协议家族中的某一种协议(AF_INET表示IPV4)
- type:套接字类型(SOCK_STREAM表示面向字节流,例如TCP;SOCK_DGRAM表示面向报文段,例如UDP)
- protocol:该协议指定要与套接字一起使用的特定协议(一般情况下为0)
- 返回值:文件描述符,创建的socket所在的文件描述符。
- socket本质是一个文件,网络通信在linux下也被看做文件。
- 绑定端口号 (TCP/UDP, 服务器)。
- 第一个参数是套接字的文件描述符。
- 第二个参数是传入的ip和port。
- 第三个参数传入的是sockaddr的长度。
- 第二个参数也可以是void类型,但是由于时代原因,没有使用void。
- 返回值为-1,则绑定失败。
剩下的接口:
开始监听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等),然而各种网络协议的地址格式并不相同。
- 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结构体指针做为参数。
sockaddr_in的源码:
下面这些接口用于将字符串风格的ip地址或者端口号与整形的ip地址和端口号之间相互转换。
UDP下的接收数据和发送数据的接口:
- flag表示等待方式,0表示阻塞等待。
- 将接收数据存放在缓冲区buf内。
- len表示你希望读取的长度。
- src_addr是一个输出型参数,用来拿到发送端的sockaddr信息。
- 最后一个参数既是输出型参数,也是输入型参数。它必须先被src_addr的大小进行初始化,不然就会出错。
- 返回值是实际收到的字符个数。
- buf表示你想发送数据。
- len表示buf的长度。
- flag一般给0,表示阻塞等待。
- 最后两个参数表示你想往哪发,即服务器对应的sockaddr和其长度。
一个查看网络状态的命令:
netstat #查看网络情况
netstat -nlup #显示udp进程信息
netstat -nltp #显示number list tcp process ,tcp进程信息
一个简易版的udp套接字
/************* udpService.hpp *****************/
1 #pragma once
2 #include <iostream>
3 #include <string>
4 #include <unistd.h>
5 #include <netinet/in.h>
6 #include <arpa/inet.h>
7 #include <sys/types.h>
8 #include <sys/socket.h>
9 class UdpService{
10 std::string ip;
11 int port;
12 int sockfd;
13 public: //127.0.0.1 本地环回,用来代码测试,一台机器上实现通信。
14 UdpService(std::string _ip = "127.0.0.1", int _port = 8080)
15 : ip(_ip), port(_port){}
16
17 void UdpServiceInit(){
18 sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建套接字
19
20 //创建好套接字后,套接字只有文件信息;而我们的ip和port信息
21 //还在用户层,我们需要通过socket文件,将ip和port信息传入,
22 //让系统知道;将文件与网络信息关联起来;这个过程叫做绑定,bind.
23
24 //服务器一般是被动接收信息,绑定的套接字是自己的ip地址和端口号,
25 //目的是让别人找到我。
26 struct sockaddr_in serSock;
27 serSock.sin_family = AF_INET;
28 serSock.sin_port = htons(port);
29 serSock.sin_addr.s_addr = inet_addr(ip.c_str());
30
31 if(bind(sockfd, (struct sockaddr*)&serSock, sizeof(serSock)) < 0){
32 std::cerr << "bind error" << std::endl;
33 exit(1);
34 }
35 }
36
37 void Start(){
38 char msg[64] = {0};
39 for(;;){
40 //ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
41 //struct sockaddr *src_addr, socklen_t *addrlen);
42 struct sockaddr_in temp;
43 socklen_t len = sizeof(temp);
44 ssize_t s = recvfrom(sockfd, msg, sizeof(msg) - 1, 0, (struct sockaddr*)&temp, &len);
45 if(s > 0){
46 msg[s] = 0;
47 std::cout << msg << std::endl;
48 }
49 //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
50 // const struct sockaddr *dest_addr, socklen_t addrlen);
51 std::string str = msg;
52 str += "[Service]";
53 sendto(sockfd, str.c_str(), str.size(), 0, (sockaddr*)&temp, sizeof(temp));
54 }
55 }
56 ~UdpService(){
57 close(sockfd);
58 }
59 };
/************** udpClient.hpp ****************/
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
class UdpClient{
std::string ip;
int port;
int sockfd;
public: //127.0.0.1 本地环回,用来代码测试,一台机器上实现通信。
//使用的是服务器的ip地址和端口号来初始化。
UdpClient(std::string _ip = "127.0.0.1", int _port = 8080)
: ip(_ip), port(_port){}
void UdpClientInit(){
//客户端不需要主动绑定,操作系统帮助我们绑定。
sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建套接字
}
void Start(){
char msg[64] = {0};
for(;;){
std::string clistr;
std::cout << "Please enter message# " ;
std::cin >> clistr;
if(clistr == "quit"){
break;
}
struct sockaddr_in serSock;
serSock.sin_family = AF_INET;
serSock.sin_port = htons(port);
serSock.sin_addr.s_addr = inet_addr(ip.c_str());
sendto(sockfd, clistr.c_str(), clistr.size(), 0, (sockaddr*)&serSock, sizeof(serSock));
ssize_t s = recvfrom(sockfd, msg, sizeof(msg)-1, 0, nullptr, nullptr);
if(s > 0){
msg[s] = 0;
std::cout << msg << std::endl;
}
}
}
~UdpClient(){
close(sockfd);
}
};
/************* udpService.cc ***************/
1 #include "udpService.hpp"
2
3 int main(){
4 UdpService *up = new UdpService();
5 up->UdpServiceInit();
6 up->Start();
7 return 0;
8 }
/************** udpClient.cc*********************/
1 #include "udpClient.hpp"
2
3 int main(){
4 UdpClient* uc = new UdpClient();
5 uc->UdpClientInit();
6 uc->Start();
7 }