Socket网络编程

一、Socket简介

1、网络中进程间通信

本机进程使用进程号来区分不同的进程。进程间的通信方式有管道、信号、消息队列、共享内存、信号量等。
而网络中进程间通信,首先需要知道进程所在的主机,即网络中唯一的标识即网络层的IP地址,主机上的进程可以通过传输层的协议和端口号识别。

2、Socket原理

Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,是一种编程接口。Socket屏蔽了不同网络协议的差异,支持面向连接(TCP)和无连接(UDP)。
5329931c43f7ee7847d4941bb1207a9.png

二、Socket编程的基础知识

1、网络字节序

字节序在内存中存储的方式有大端序(网络序)和小端序(主机序)。
大端序:将高字节存储在低地址。
小端序:将低字节存储在低地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 此程序用于判断本机的字节序
#include <iostream>
#include <cstdio>

using namespace std;

int main()
{
int x = 0x123456;
char *p = (char *)&x;
if(p[0] == 0x12) cout << "大端" << endl;
else cout << "小端" << endl;
return 0;
}

网络中在处理多字节时一般采用大端序,在网络传输中需要把主机字节序转换到网络字节序。常用的的转换函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<arpa/inet.h>

//主机序转换为网络序
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主机

to: to转为

n: net网络

l: long长整型

s: short短整型
*/

2、sockaddr结构体

存放协议族、端口和地址信息,客户端的connect()函数和服务端的bind()函数需要这个结构体。

1
2
3
4
struct sockaddr {
unsigned short sa_family; // 协议族,与socket()函数的第一个参数相同,填 AF_INET。
unsigned char sa_data[14]; // 14字节的端口和地址。
}

3、sockaddr_in结构体

sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便。所以定义了等价的sockaddr_in 结构体,它的大小与sockaddr相同,实际使用可以强制转换成sockaddr。

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in {
unsigned short sin_family; // 协议族,与 socket()函数的第一个参数相同,填 AF INET.
unsigned short sin_port; // 16 位端口号。用 htons()把整数的端口转换一下就可以了。
struct in_addr sin_addr; //IP 地址的结构体。
unsigned char sin_zero[8]; // 未使用,为了保持与 struct sockaddr 一样的长度而添加。
}

//IP 地址的结构体。
struct in_addr {
unsigned int s_addr; // 32 位的 IP 地址,大端序。
}

4、字符串IP与大端序IP的转换

C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通讯的服务端程序中。

1
2
3
4
5
6
7
8
9
10
typedef unsigned int in_addr_t;// 32 位大端序的 IP 地址。

// 把字符串格式的IP转换成大端序的IP,转换后的IP赋给sockaddr_in.in_addr.s_addr。
in_addr_t inet_addr(const char *cp);

//把字符串格式的IP转换成大端序的IP,转换后的IP将填充到sockaddr_in.in_addr 成员。
int inet_aton(const char *cp, struct in_addr *inp);

//把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址。
char *inet_ntoa(struct in_addr in);

三、Socket网络编程的流程

3537d9651d3ba01589078ba28e82a15.png

四、代码

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//此程序用于演示socket通讯的客户端。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc,char *argv[])
{
if (argc!=3)
{
printf("Using:./demo01 ip port\nExample:./demo01 127.0.0.1 5005\n\n"); return -1;
}

// 第1步:创建客户端的socket。
int sockfd;
if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }

// 第2步:向服务器发起连接请求。
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通讯端口。

//用于存放服务端!P地址(大端序)的结构体的指针。
struct hostent *h; //把域名/主机名/字符串格式的IP转换成结构体。
if( (h = gethostbyname(argv[1])) == 0 ) // 指定服务端的ip地址
{ printf("gethostbyname failed.\n"); close(sockfd); return -1; }
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);

//servaddr.sin_addr.s_addr = inet_addr(argv[1]); // 指定服务端的IP,只能用IP,不能用 域名 和 主机名

if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0) // 向服务端发起连接清求。
{
perror("connect"); close(sockfd); return -1;
}

int iret;
char buffer[102400];

// 第3步:与服务端通讯,发送一个报文后等待回复,然后再发下一个报文。
for (int ii=0;ii<10;ii++)
{
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1);
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // 向服务端发送请求报文。
{ perror("send"); break; }
printf("发送:%s\n",buffer);

memset(buffer,0,sizeof(buffer));
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // 接收服务端的回应报文。
{
printf("iret=%d\n",iret); break;
}
printf("接收:%s\n",buffer);

sleep(1); // 每隔一秒后再次发送报文。
}

// 第4步:关闭socket,释放资源。
close(sockfd);
}

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//此程序用于演示socket通讯的服务端。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc,char *argv[])
{
if (argc!=2)
{
printf("Using:./demo02 port\nExample:./demo02 5005\n\n"); return -1;
}

// 第1步:创建服务端的socket。
int listenfd;
if ( (listenfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }

// 第2步:把服务端用于通讯的地址和端口绑定到socket上。
struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。

servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
servaddr.sin_port = htons(atoi(argv[1])); // 指定通讯端口。

if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{ perror("bind"); close(listenfd); return -1; }

int opt = 1; unsigned int len = sizeof(opt);
// 一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。
// 与Tcp四次挥手的Time wait状态有关
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,len);

// 第3步:把socket设置为监听模式。
if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }

// 第4步:接受客户端的连接。
int clientfd; // 客户端的socket。
int socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小
struct sockaddr_in clientaddr; // 客户端的地址信息。

/*
如果accept成功返回,则服务器与客户端已经正确连接了。服务器通过返回的套接字来完成与客户端的通信。
accept默认会阻塞进程直到有一个客户连接建立后返回返回的是一个新可用的连接套接字。
accept函数返回的是连接套接字,代表与客户端已经建立连接
*/
clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
printf("客户端(%s)已连接。\n",inet_ntoa(clientaddr.sin_addr));

int iret;
char buffer[102400];

// 第5步:与客户端通讯,接收客户端发过来的报文后,回复ok。
while (1)
{
memset(buffer,0,sizeof(buffer));
if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) // 接收客户端的请求报文。
{
printf("iret=%d\n",iret); break;
}
printf("接收:%s\n",buffer);

strcpy(buffer,"ok");
if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) // 向客户端发送响应结果。
{ perror("send"); break; }
printf("发送:%s\n",buffer);
}

// 第6步:关闭socket,释放资源。
close(listenfd); close(clientfd);
}