【笔记】Socket学习笔记

目录

一、Socket主要概念

二、Linux下的socket代码分析

三、实现一个Linux的socket程序

一、Socket主要概念

端口

有了 IP 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如Web服务、FTP服务(文件传输服务)、SMTP服务(邮箱服务)等,仅有 IP 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号

常用数据传输方式

SOCK_STREAM 和 SOCK_DGRAM。

  1. SOCK_STREAM表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析
  2. SOCK_DGRAM 表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM所做的校验工作少,所以效率比 SOCK_STREAM高

在 socket 编程中,需要同时指明数据传输方式和协议。

IP地址和端口能够在广袤的互联网中定位到要通信的程序,协议和数据传输方式规定了如何传输数据,有了这些,两台计算机就可以通信了。

Windows和Linux下socket的区别

  • Linux不用加载dll文件,而Windows下的socket程序依赖Winsock.dll或ws2_32.dll,必须提前加载

  • Linux 使用“文件描述符”的概念,而 Windows 使用“文件句柄”的概念;Linux 不区分 socket 文件和普通文件,而 Windows 区分;Linux 下 socket() 函数的返回值为 int 类型,而 Windows 下为 SOCKET 类型,也就是句柄

  • Linux 下使用 read() / write() 函数读写,而 Windows 下使用 recv() / send() 函数发送和接收

  • 关闭 socket 时,Linux 使用 close() 函数,而 Windows 使用 closesocket() 函数

二、Linux下的socket代码分析

Linux创建socket()程序的核心函数

socket()函数创建套接字-返回一个套接字(服务器端int serv_sock,客户端 int sock)

socket()存在于头文件<sys/socket.h>中,函数原型为

1
2
3
4
5
int socket(int af, int type, int protocol);

# af:AF_INET、AF_INET6
# type:SOCK_STREAM、SOCK_DGRAM
# protocol:IPPROTO_TCP、IPPTOTO_UDP、0((0表示自动选择)

服务器端的bind()函数

bind() 函数将套接字与特定的IP地址和端口绑定起来,函数原型

1
2
3
4
5
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);

# sock是用socket创建的套接字
# sockaddr *addr一般写成(struct sockaddr*)&serv_addr
# addrlen一般写成sizeof(serv_addr)
结构体sockaddr_in
1
2
3
4
5
6
7
8
9
struct sockaddr_in{
//和socket() 的第一个参数的含义相同,取值也要保持一致
sa_family_t sin_family;
//尽量在 1024~65536 之间分配端口号,且要用htons() 函数转换
uint16_t sin_port;
struct in_addr sin_addr;
//一般用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero自然就是0
char sin_zero[8];
};
结构体in_addr
1
2
3
struct in_addr{
in_addr_t s_addr;
};

所以在给serv_addr.sin_addr.s_addr赋值时要用inet_addr()对字符串形式的ip地址进行转换

采用sockaddr_in强转成sockaddr形式的原因

  • sockaddr有sin_family(2)和sa_data(14),要给sa_data 赋值,必须同时指明IP地址和端口号,但缺少相关函数将”127.0.0.1:80”转换成需要的形式,因此不直接用sockaddr
  • sockaddr_in和sockaddr都是16字节,强转不会丢失字节,可以理解为sockaddr_in专为IPv4地址设计,sockaddr则更通用
1
2
3
4
5
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);

客户端的connect()函数

与服务器端的bind()相同,函数原型

1
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);

服务器端的listen()函数

listen() 函数让套接字进入被动监听状态,函数原型为

1
2
3
4
int listen(int sock, int backlog);

# sock一般为server.cpp开头定义的serv_sock
# backlog大小为请求队列大小,该缓冲区满时就不再接受新请求,可以是具体数字也可以是SOMAXCONN(由系统自动决定)

服务器端的accept()函数-返回一个套接字(int clnt_sock =)

套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求,函数原型

1
2
3
4
5
6
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

# sock一般为server.cpp开头定义的serv_sock
# sockaddr *addr一般写成(struct sockaddr*)&clnt_addr
# *addrlen一般写成&clnt_addr_size
# ps:clnt_addr是新定义的sockaddr_in结构保存了客户端的ip地址和端口号,且socklen_t clnt_addr_size = sizeof(clnt_addr)

accept()会阻塞程序执行,直到接受到客户端的请求

服务器端的write()函数

服务器端用 write() 向套接字写入数据,函数原型

1
2
3
4
5
ssize_t write(int fd, const void *buf, size_t nbytes);

# fd是accept()返回的客户端的套接字clnt_sock
# *buf是要传的字符串变量,例str
# nbytes是要传的数据大小,例sizeof(str)

客户端的read()函数

客户端用read()从套接字读出数据,函数原型

1
2
3
4
5
ssize_t read(int fd, void *buf, size_t nbytes);

# fd是client.cpp开头定义的套接字sock
# *buf是预先声明的为数据准备的缓冲区,即变量buffer
# nbytes是要读取的字节数,一般用sizeof(buffer)-1

三、实现一个Linux的socket程序

服务器端代码server.cpp

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
//创建套接字,参数AF_INET表示使用IPv4 地址,SOCK_STREAM 表示使用面向连接的数据传输方式,IPPROTO_TCP表示使用TCP协议
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
//每个字节都用0填充
memset(&serv_addr, 0, sizeof(serv_addr));
//使用IPv4地址
serv_addr.sin_family = AF_INET;
//具体的IP地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//端口
serv_addr.sin_port = htons(1234);
//通过 bind() 函数将套接字 serv_sock 与特定的IP地址和端口绑定,IP地址和端口都保存在 sockaddr_in 结构体中
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//进入监听状态(处在睡眠),等待用户发起请求才会被唤醒
listen(serv_sock, 20);
//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
char str[] = "Hello World!";
//write() 函数用来向套接字文件中写入数据,也就是向客户端发送数据
write(clnt_sock, str, sizeof(str));

//关闭套接字
close(clnt_sock);
close(serv_sock);
return 0;
}

socket() 函数确定了套接字的各种属性,bind() 函数让套接字与特定的IP地址和端口对应起来,这样客户端才能连接到该套接字。

客户端代码client.cpp:

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
//向服务器(特定的IP和端口)发起请求
struct sockaddr_in serv_addr;
//每个字节都用0填充
memset(&serv_addr, 0, sizeof(serv_addr));
//使用IPv4地址
serv_addr.sin_family = AF_INET;
//具体的IP地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//端口
serv_addr.sin_port = htons(1234);
//通过 connect() 向服务器发起请求,服务器的IP地址和端口号保存在 sockaddr_in 结构体中。直到服务器传回数据后,connect() 才运行结束
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

char buffer[40];
//通过 read() 从套接字文件中据读取服务器传回的数据
read(sock, buffer, sizeof(buffer)-1);

printf("Message form server: %s\n", buffer);

//关闭套接字
close(sock);
return 0;
}

验证步骤

1
2
g++ server -o server
./server

再打开另一个控制台

1
2
g++ client -o client
./client

若打印出现

1
Messege from sercer:Hello World

则表示客户端和服务端通过socket完成了通信

附加1:回声客户端的实现

功能要求:服务器端发送data给客户端后,客户端返回该data给服务器端

在server.cpp中加入

1
read(clnt_sock, buffer, sizeof(buffer)-1);

在client.cpp中加入

1
write(sock, buffer, sizeof(buffer));

结论:服务器端也能用read(),客户端也能用write()

附加2:迭代服务器端和客户端

功能要求:在回声的基础上一直循环下去

server.cpp改成

1
2
3
4
5
6
7
......
while(1){
int clnt_sock = accept(serv_sock, (sock_addr*)&clnt_addr, &clnt_addr_size;
......
close(clnt_sock);
}
close(clnt_sock)

client.cpp改成

1
2
3
4
5
6
7
......
while(1){
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, (sock_addr*)&serv_addr, sizeof(serv_addr));
.......
close(sock);
}

思路:每次循环服务器端accept收到一个新的客户端套接字,循环完成后关闭该套接字;
每次循环客户端新生成套接字并发送连接请求,循环完成后关闭该套接字。

遇到的问题

  1. 第一遍A窗口运行server,再打开B窗口运行client一切,但是不关闭A窗口再一次启动server后,无论是重新启动B窗口运行client还是在原来的B窗口继续运行client,都无法实现通信,server一直卡在accept,client收到乱码,为什么?
  2. client打印的/n无法实现正常换行,应该怎么改? 斜杠又打错了,是\n