今天继续对socket编程进行研究,这里会真正开如用socket写一个小例子,进入正题:
TCP客户/服务器模型:
![](https://i-blog.csdnimg.cn/blog_migrate/122f052b8f1dcdadf99676793a6193f1.png)
关于这个模型的流程这里就不多说了,比较容易理解,下面则利用这种模型来编写一个实际的例子。
回射客户/服务器:
![](https://i-blog.csdnimg.cn/blog_migrate/467c45eef0403c3ed632dc22be80d582.png)
这个例子的效果就是:客户端从命令行获取一行命令,然后发送给服务器端,当服务端接收到这行命令之后,不做任何操作,将其又回送给客户端,然后客户端进行回显,下面则开始一步步来实现这样的效果,来初步感受下Socket编程:
首先编写服务端:echosrv.c
第一步:创建套接字:
![](https://i-blog.csdnimg.cn/blog_migrate/51c873b6184f4e2d295aeff0b48bbea6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7bc9cbc1707158212d3a2eb55a2ce845.png)
关于第一个参数domain,man帮助中也有说明:
![](https://i-blog.csdnimg.cn/blog_migrate/ebdb3abdb48dac8c8b7e4288003d1901.png)
但是,AF_INET等价于PF_INET,这里推荐用后者,因为刚好代表protocol family含义,下面代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/9a27afae60169c169310d588e48553d3.png)
第二步:绑定一个地址到套接字上:
![](https://i-blog.csdnimg.cn/blog_migrate/586489f0fffbf34f9c6eef4aa2d618ae.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4d035cd26f2d017b9ad0a9f98cf334dd.png)
首先准备一下第二个参数,也就是要绑定的地址:
![](https://i-blog.csdnimg.cn/blog_migrate/e059c67be9568396e361625a08677f09.png)
其中绑定地址还有其它两种方式:
![](https://i-blog.csdnimg.cn/blog_migrate/6b8df8fc66f6412585ff488675277c6f.png)
另外,其实"servaddr.sin_addr.s_addr = htonl(INADDR_ANY);"这种写法是可以省略掉的,因为它是全0,但这里为了显示说明所以保留。
【提示】:关于上面的写法,可以参考博文:http://www.cnblogs.com/webor2006/p/3905799.html 中的
地址转换函数
下面开始进行绑定:
![](https://i-blog.csdnimg.cn/blog_migrate/2a724371da89bd29f2ae17d0068a4750.png)
第三步:则开始进行监听:
![](https://i-blog.csdnimg.cn/blog_migrate/f17f2094d9ae588c2c365fa2731a5284.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e25cc839c13ffc46fe15e731b20d6538.png)
![](https://i-blog.csdnimg.cn/blog_migrate/d2bf566192e9d511e38805b3004b0911.png)
![](https://i-blog.csdnimg.cn/blog_migrate/56c501d8cde7bdc84c765a900a8e0960.png)
具体代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/36d11bfb4df0f0b67d3c22e392e6a7af.png)
其中SOMAXCONN可以从man帮助中查看到:
![](https://i-blog.csdnimg.cn/blog_migrate/4b207d598dcd9fda3311de7dda51ec19.png)
它代表了socket的并发最大连接个数。
另外还得注意,
套接字有被动套接字和主动套接字之分,当调用listen之后,该socket就变动被动套接字了,需要由主动套接字来发起连接,主动套接字是用connect函数来发起连接的。
第四步:从已完成连接队列中返回第一个连接:
![](https://i-blog.csdnimg.cn/blog_migrate/a611011175a729f3d5c6464b0fb91118.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b50b77719287e1005240a4887666f8c2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/6a341f15ecb10f0501efe75b344bfe6b.png)
接下来,则进行数据的接收,并将数据回显给客户端:
accept函数会返回一个新的套接字,注意:此时的套接字不再是被动套接字,而变为了主动:
![](https://i-blog.csdnimg.cn/blog_migrate/352d47cf7c3ce7951c917d5e9a2c3ba4.png)
可以通过accept的man手册来得知:
![](https://i-blog.csdnimg.cn/blog_migrate/6cc01b9a5b4d52eec04c6c8a2c38d29d.png)
下面,则开始从该套接字中读取客户端发过来的数据:
![](https://i-blog.csdnimg.cn/blog_migrate/a5d59df771549424161b832d13d054c4.png)
至此,服务端的代码都已经编写完了,下面则先编译一下:
![](https://i-blog.csdnimg.cn/blog_migrate/192d9dd32ea270fb547730a7fce74e1b.png)
查看man帮助:
![](https://i-blog.csdnimg.cn/blog_migrate/9dbca59d6a056578b5c2aae8125df070.png)
于是在代码中加入头:
![](https://i-blog.csdnimg.cn/blog_migrate/c4109d1e6669381e2bbe26f6683368d3.png)
再次编译:
![](https://i-blog.csdnimg.cn/blog_migrate/ec3da8669cd411dd231cf25f2de4637e.png)
还是出错,那IPPPOTO_TCP是在哪定义的呢?
可以通过以下命令进行查找:
![](https://i-blog.csdnimg.cn/blog_migrate/2b0a9af1994fcb909e605fa3ac566c5d.png)
于是乎,加上该头文件后再编译:
![](https://i-blog.csdnimg.cn/blog_migrate/b62fc767d1eaccc24a7e3f0ef16b8370.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f881e2b299c40249e9b4998ea879f28b.png)
用同样的办法来进行查找:
![](https://i-blog.csdnimg.cn/blog_migrate/dc516c1a763796fb82013a64bd02d558.png)
于是加入它:
![](https://i-blog.csdnimg.cn/blog_migrate/eb44a7c6bab1bfa889450d95fe9c9f03.png)
再次编译:
![](https://i-blog.csdnimg.cn/blog_migrate/1bf7e6761f5e9fdef47fc55fc6598119.png)
还是报错,对于这里面对应的头文件这里就不具体一个个查找了,不然有点充数的嫌疑,将所有头文件加上再次编译:
![](https://i-blog.csdnimg.cn/blog_migrate/307867305fa2e7bdabbd927986ce7595.png)
接下来,开始编写客户端的代码:echocli.c
首先创建一个socket:
![](https://i-blog.csdnimg.cn/blog_migrate/4989156362c8876d181e779982506228.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c1669f013409cc3dab34c5ae524e6022.png)
第二步开始与服务器进行连接:
![](https://i-blog.csdnimg.cn/blog_migrate/1aa6e068dd483896ec9477fc4ea7b8c3.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a71f3b4235f11de1a476b45a895a6c2d.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c189e54343788da387318276399a008c.png)
【说明】:用connect发起连接的套接字是主动套接字。
连接成功之后,就可以向服务器发送数据了:
![](https://i-blog.csdnimg.cn/blog_migrate/4f99656a079b62bfc1b1b7daca44c2b3.png)
另外,服务端在使用资源之后,最后也得关闭掉,所以修改服务端程序如下:
![](https://i-blog.csdnimg.cn/blog_migrate/6924e9c74610dbf888a08a3758e3d9b4.png)
这时,客户端程序也已经编写完成,下面编译运行看一下效果:
![](https://i-blog.csdnimg.cn/blog_migrate/b060e5b88eb91fed1660d3d7b35beab0.gif)
也就是第一次客户端输入很长的字符串,而第二次输入很短的字符串时,这时就会输出有问题,照理应该是客户端输入什么,服务端就会回显给客户端,也就是打印两条一模一样的语句,产生这样的问题原因是什么呢?
下面来用图来分析一下:
![](https://i-blog.csdnimg.cn/blog_migrate/9b96e4ffc9b528eef831719475553537.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c440bcdc23290e0915fb60335dccf48f.png)
所以,解决该问题的思路就是每次循环时将值初始化一下既可,修改代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f95011d827990fe9a6f684914f8586d.png)
再次编译运行:
![](https://i-blog.csdnimg.cn/blog_migrate/ceb2743e8867b5713c5945996af6ee1a.png)
关于这个原因,之后会来解决,先占且不关心,等一会就会正常了,正常运行的效果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/aaf643faa26a8bd231618e99fb9f7f1c.gif)
这样,就实现了客户端与服务器端的socket通讯了,最终的代码如下:
echosrv.c【服务端】:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/* if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1", &servaddr.sin_addr);*/
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
ERR_EXIT("accept");
char recvbuf[1024];//用来存储客户端发来的数据
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));//从套接字中读取数据
fputs(recvbuf, stdout);//打印到屏幕上
write(conn, recvbuf, ret);//并且将其又回显给客户端,其第三个参数的长度正好是我们接收到的长度
}
close(conn);
close(listenfd);
return 0;
}
echocli.c【客户端】:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect");
char sendbuf[1024] = {0};
char recvbuf[1024] ={0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sock, sendbuf, strlen(sendbuf));
read(sock, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
return 0;
}
好了,今天的内容学到这,下回见~