第2关:socket编程
- 任务描述
- 相关知识
- 1、Socket
- 2、socket函数
- 3、bind函数
- 4、listen、connetct函数
- 5、accept函数
- 6、read、write函数
- 7、close、shutdown函数
- 8、三次握手
- 9、数据传输
- 10、四次挥手
- 编程要求及注意事项
- 测试说明
- 参考代码
任务描述
本关任务:
编写两个程序实现客户端与服务端的通信,clinet.c为客户端,server.c为服务器端。服务器端能够读取客户端发送的信息。
相关知识
为了完成本关任务,你需要掌握:1.socket编程,2.数据通信过程。
1、Socket
网络中进程可以通过socket通信,socket起源于Unix,满足“一切皆文件”原理,即操作为“打开open->读写write/read->关闭close”。
①socket的调用函数主要有:
socket()/*创建描述符,设定协议域和socket类型*/
bind()/*绑定地址*/
listen()/*监听*/
connect()/*连接*/
read()/*I/O读操作*/
write()/*I/O写操作*/
close()/*断开连接*/
shutdown()/*部分断连*/
②socket操作主要有三类:
1)三次握手:让客户端与服务端双方都能明确自己和对方的收、发能力是正常的。
2)数据传输:socket使用的是TCP连接,为双向传输的对等模式,双方都可以同时向对方发送或接收数据。
3)四次挥手:由于TCP连接是全双工,每个方向都必须单独进行关闭。
2、socket函数
int socket(int domain,int type,int protocol)
sockfd=socket();
socket()创建一个socket描述符,标识唯一一个socket
domain:协议域->AF_INET、AF_INET6、AF_LOCAL(AF_UNIX)、AF_ROUTE。协议族决定socket地址类型,通信中必须采用对应的地址,AF_INET决定要用ipv4地址(32位)与端口号(16位)组合看,AF_UNIX决定用一个绝对路径作为地址。
type:socket->指定socket类型。常用SOCK_STREAM(TCP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET、SOCK_DGRAM(UDP)等
protocol:协议->常用协议IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输、UDP传输、STCP传输、TIPC传输
注:type和protocol不可用随意组合,SOCKET_STREAM和IPPROTO_UDP不可用组合。当protocol为0,会自动选择type类型对应默认协议。
socket函数创建一个socket时,返回的socket描述符存在于协议族address family中,没有具体地址,赋值一个地址必须调用bind()或者调用connect()、listen()系统自动随机分配端口。
3、bind函数
bind()函数把一个地址族中特定地址赋值给socket,AF_INET、AF_INET6就是一个ipv4或ipv6地址和端口号组合,赋值给socket。
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
sockfd:socket描述符,通过socket()创建,唯一标识一个socket。bind()将这个描述符绑定一个名字。
addr:const struct sockaddr*指针,指向sockfd的协议地址。地址结构根据创建socket时的地址协议族不同而变化。
addrlen:地址长度
注:①服务器启动时会绑定地址(ip+端口号)。提供服务时,服务端通过地址连接服务器。客户端不用绑定,系统自动分配端口号和自身iP地址组合。所以通常服务端listen前会调用bind(),客户端不会调用,在connect()时系统随机生成一个。
②主机字节序(大端小端模式):不同CPU有不同字节序类型,这些字节序指整数在内存中保存的顺序。
1)little-endian,低位字节放内存低地址段,高位字节排放内存高地址端。
2)big-endian,高位字节放内存低地址段,低位字节放内存高地址端。
网络字节序:4个字节的32bit值以下次序传输,0-7、8-15、16-23、24-31。大端字节序。由于TCP/IP首部中所有二进制整数在网络中传输时都要求以这种次序,因此又称作网络字节序。字节序,发育一个字节类型数据在内存中存放顺序,一个字节的数据没有顺序问题。
在绑定地址到socket时候,必须将主机字节序转换成网络字节序,不能让主机字节序跟网络字节序一样使用big-endian。
4、listen、connetct函数
服务器调用socket()、bind()后就会调用listen()监听socket,如果客户端这时候调用connect()发出连接请求,服务器就会接受这个请求。
int listen(int sockfd,int backlog);
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
listen中sockfd为要监听的socket描述符,backlog为相应socket可以排队的最大连接个数。
socket()函数创建的socket是一个主动类型,listen()将socket变为被动类型等待客户连接请求。
connect中sockfd为客户端socket描述符,addr为服务器socket地址,addrlen为socket地址长度。客户端调用connect函数建立与TCP服务器的连接。
5、accept函数
TCP服务器调用socket()、bind()、listen()后,监听socket地址。TCP客户端调用socket()、connect()后,给TCP服务器发送一个连接请求。TCP服务器监听到请求,调用accept()接收请求,连接建立好开始I/O操作。
int accept(int sockfd,struct sockaddr *addr,socklen_t addrlen)
sockfd服务器socket描述符
addr返回客户端协议地址
addrlen协议地址长度
accept成功,返回值由内核自动生成的全新描述符,表示已与返回客户的TCP连接。
注:sockfd是服务器的socket描述符,服务器开始调用socket()生成的,即监听socket描述符,accept返回的是已连接socket描述符,一个服务器通常只创建一个监听socket描述符,在服务器生命周期内会一直存在,内核为每个服务器进程接收的客户连接创建一个已连接socket描述符,当服务器完成客户服务,相应已连接socket描述符会被关闭。
6、read、write函数
服务器与客户建立好连接,调用网络I/O进行读写操作,网络中不同进程之间通信。
网络I/O操作
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom/sendto()
7、close、shutdown函数
服务器与客户端建立连接后,读写操作完,需要关闭相应的socket描述符。
#include<unistd.h>
int close(int fd);
sockfd不能在作为read或者write的第一个参数。
注:close操作只是将相应的sockfd描述字引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
shutdown()函数,可以关闭socket一端或者全部。
int shutdown(int _fd,int _how)
TCP连接是双向的(可读可写),使用close读写通道都关闭,使用shutdown有三种:
①howto=0,关闭读通道,可以继续写。
②howto=1,关闭写通道,只可以读。
③howto=2,关闭读写通道,和close一样,全部关闭。
8、三次握手
tcp建立连接进行“三次握手”:
①客户端向服务器发送一个SYN x。
②服务器向客户端发送一个SYN y,并对SYN x进行ACK x+1。
③客户端再向服务器发一个ACK y+1。
①客户端调用connect触发连接请求,向服务器发送SYN x包,connect进入阻塞状态;
②服务器listen监听到连接请求(收到SYN x),调用accept接受请求,向客户端发送SYN y和ACK x+1,accept进入阻塞状态;
③客户端收到服务器的SYN y和ACK x+1,对SYN y确认,connect返回ACK y+1;
④服务器收到ACK y+1,accept返回,三次握手完毕,连接建立。
注:客户端的connect在三次握手的第二次握手返回,服务端的accept在三次握手的第三次握手返回。
9、数据传输
①建立完连接后,进行数据传输,read和write。
②客户端发起给服务端写入,并发送SYN x+1和ACK y+1给服务端(xy未发生变化)。
③服务端发起给客户端读取,并发送ACK x+2。
10、四次挥手
①客户端应用进程调用close主动关闭连接,客户端TCP发送FIN x+2和ACK y+1。
②服务端接受到FIN x+2,执行被动关闭,对FIN确认。即当服务端收到FIN后停止数据的操作并发出一个ACK x+3客户端表示数据操作结束。
③一段时间后,服务端应用接收到文件结束符的应用进程调用close关闭服务端socket,发送一个FIN。(表示socket已经关闭)
④客户端接收到FIN,关闭客户端的socket,发送ACK y+2高速服务端。
编程要求及注意事项
根据client.c代码及提示,在右侧编辑器中选中server.c注释处补充代码。
测试说明
平台会对你编写的代码进行测试,比对输出结果。
开始你的任务吧,祝你成功!
参考代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 4096
int main(int argc, char *argv[])
{
int sockfd, n; // sockfd是客户端创建的socket描述字,客户端不需要区分监听listenfd和连接connfd。n为发送长度
char recvline[MAXLINE], sendline[MAXLINE]; // recvline暂时没用,sendline为即将发送的数据
struct sockaddr_in servaddr; //创建需要连接的服务端地址
if (argc != 2)
{
printf("usage: ./client <ipaddress>");
exit(0);
} //设置运行时候需要输入的格式,执行文件+服务端ip地址
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("create socket error:%s(errno:%d)\n", strerror(errno), errno);
exit(0);
} //创建socket描述字,因为客户端,只需要一次连接
memset(&servaddr, 0, sizeof(servaddr)); //先把当前连接的服务端地址全填充0
servaddr.sin_family = AF_INET; //目的服务端地址协议簇为ipv4
servaddr.sin_port = htons(6666); //设置端口号6666
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
{
printf("inet_pton error for %s\n", argv[1]);
exit(0);
} //将目的服务端的地址,转换成网络地址,必须转换成网络地址!!argv[0]是./client,argv[1]是服务端的地址
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect socket error:%s(errno:%d)\n", strerror(errno), errno);
exit(0);
} //创建连接,本机的socket通过服务端地址连接服务端,强转指针格式
printf("send msg to server:\n"); //连接上后输出发送信息到服务端
fgets(sendline, 4096, stdin); //标准输入流中获取需要发送的信息,存储到sendline字符数组中
if (send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error:%s(errno:%d)\n", strerror(errno), errno);
exit(0);
} //发送信息,sendline buff中的信息
close(sockfd); //信息发送完关闭自己的sockfd,第一次挥手
exit(0);
}