目录
一、Socket主要概念
二、Linux下的socket代码分析
三、实现一个Linux的socket程序
一、Socket主要概念
端口
有了 IP 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如Web服务、FTP服务(文件传输服务)、SMTP服务(邮箱服务)等,仅有 IP 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。
为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号
常用数据传输方式
SOCK_STREAM 和 SOCK_DGRAM。
- SOCK_STREAM表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析
- 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.可以用cout和cin
2.不能用write和read传string类型变量
遇到的问题
- 第一遍A窗口运行server,再打开B窗口运行client一切,但是不关闭A窗口再一次启动server后,无论是重新启动B窗口运行client还是在原来的B窗口继续运行client,都无法实现通信,server一直卡在accept,client收到乱码,为什么?
client打印的/n无法实现正常换行,应该怎么改? 斜杠又打错了,是\n