websocket同步

tech2022-09-06  110

websocket同步

This article was peer reviewed by Wern Ancheta. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

本文由Wern Ancheta进行同行评审 。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

I’m always yammering on about writing asynchronous PHP code, and for a reason. I think it’s healthy to get fresh perspectives – to be exposed to new programming paradigms.

由于某种原因,我总是对编写异步PHP代码感到厌烦。 我认为获取新鲜的观点是健康的-接受新的编程范例。

Asynchronous architecture is common in other programming languages, but it’s only just finding its feet in PHP. The trouble is that this new architecture comes with a cost.

异步体系结构在其他编程语言中很常见,但只是在PHP中找到了立足之本。 问题在于这种新架构要付出代价。

I don’t talk about that cost enough.

我还没说足够的钱。

When I recommend frameworks like Icicle, ReactPHP, and AMPHP, the obvious place to start with them is to create something new. If you have an existing site (perhaps running through Apache or Nginx), adding daemonised PHP services to your app is probably not as easy as just starting over.

当我推荐Icicle , ReactPHP和AMPHP之类的框架时,最明显的起点就是创建一些新的东西。 如果您有一个现有站点(可能通过Apache或Nginx运行),则将守护程序PHP服务添加到您的应用程序可能不像重新开始那样容易。

It takes a lot of work to integrate new, asynchronous features into existing applications. Often there are good reasons and great benefits, but a rewrite is always a hard-sell. Perhaps you can get some of the benefits of parallel execution without using an event loop. Perhaps you can get some of the benefits of web sockets without a new architecture.

将新的异步功能集成到现有应用程序中需要大量的工作。 通常,有充分的理由和巨大的好处,但是重写始终很难。 也许您可以在不使用事件循环的情况下获得并行执行的一些好处。 也许您可以在没有新架构的情况下获得Web套接字的一些好处。

I’m going to show you a Sockets-as-a-Service service, called Socketize. Try saying that a few times, out loud…

我将向您展示一种称为Socketize的套接字即服务的服务。 尝试说几次,然后大声说...

Note: Web sockets involve a fair amount of JavaScript. Fortunately, we don’t need to set up any complicated build chains. You can find the example code for this tutorial here.

注意:Web套接字包含大量JavaScript。 幸运的是,我们不需要建立任何复杂的构建链。 您可以在此处找到本教程的示例代码。

建立! (Setup!)

Let’s set up a simple CRUD example. Download the SQL script, and import it to a local database. Then let’s create a JSON endpoint:

让我们建立一个简单的CRUD示例。 下载SQL脚本 ,并将其导入到本地数据库。 然后让我们创建一个JSON端点:

$action = "/get"; $actions = ["/get"]; if (isset($_SERVER["PATH_INFO"])) { if (in_array($_SERVER["PATH_INFO"], $actions)) { $action = $_SERVER["PATH_INFO"]; } } $db = new PDO( "mysql:host=localhost;dbname=[your_database_name];charset=utf8", "[your_database_username]", "[your_database_password]" ); function respond($data) { header("Content-type: application/json"); print json_encode($data) and exit; }

This code will decide whether a request is being made against a valid endpoint (currently only supporting /get). We also establish a connection to the database, and define a method for allowing us to respond to the browser with minimal effort. Then we need to define the endpoint for /get:

此代码将决定是否针对有效端点(当前仅支持/get )发出请求。 我们还建立了与数据库的连接,并定义了一种方法,使我们能够以最小的努力响应浏览器。 然后,我们需要为/get定义端点:

if ($action == "/get") { $statement = $db->prepare("SELECT * FROM cards"); $statement->execute(); $rows = $statement->fetchAll(PDO::FETCH_ASSOC); $rows = array_map(function($row) { return [ "id" => (int) $row["id"], "name" => (string) $row["name"], ]; }, $rows); respond($rows); }

/get is the default action. This is the thing we want this PHP script to do, if nothing else appears to be required. This will actually become more useful as we add functionality. We also define a list of supported actions. You can think of this as a whitelist, to protect the script against strange requests.

/get是默认操作。 如果没有其他要求,这就是我们希望此PHP脚本执行的操作。 实际上,随着我们添加功能,这将变得更加有用。 我们还定义了支持的操作列表。 您可以将其视为白名单,以防止脚本受到奇怪的请求。

Then, we check the PATH_INFO server property. We’ll see (in a bit) where this info comes from. Just imagine it contains the URL path. So, for http://localhost:8000/foo/bar, this variable will contain /foo/bar. If it’s set, and in the array of allowed actions, we override the default action.

然后,我们检查PATH_INFO服务器属性。 我们将(稍后)看到此信息的来源。 试想一下它包含URL路径。 因此,对于http:// localhost:8000 / foo / bar ,此变量将包含/foo/bar 。 如果已设置,并且在允许的操作数组中,我们将覆盖默认操作。

Then we define a function for responding to the browser. It’ll set the appropriate JSON content type header, and print a JSON-encoded response, which we can use in the browser.

然后,我们定义一个响应浏览器的函数。 它将设置适当的JSON内容类型标头,并打印一个JSON编码的响应,我们可以在浏览器中使用它。

Then we connect to the database. Your database details will probably be different. Pay close attention to the name of your database, your database username and database password. The rest should be fine as-is.

然后,我们连接到数据库。 您的数据库详细信息可能会有所不同。 请密切注意数据库的名称,数据库用户名和数据库密码。 其余应保持原样。

For the /get action, we fetch all rows from the cards table. Then, for each card, we cast the columns to the appropriate data type (using the array_map function). Finally, we pass these to the respond function.

对于/get操作,我们从cards表中获取所有行。 然后,对于每张卡,我们将列转换为适当的数据类型(使用array_map函数)。 最后,我们将它们传递给respond函数。

To run this, and have the appropriate PATH_INFO data, we can use the built-in PHP development server:

要运行它并获取适当的PATH_INFO数据,我们可以使用内置PHP开发服务器:

$ php -S localhost:8000 server.php

The built-in PHP development server is not good for production applications. Use a proper web server. It’s convenient to use it here, because we don’t need to set up a production server on our local machine to be able to test this code.

内置PHP开发服务器不适用于生产应用程序。 使用适当的Web服务器。 在这里使用它很方便,因为我们不需要在本地计算机上设置生产服务器即可测试此代码。

At this point, you should be able to see the following in a browser:

此时,您应该能够在浏览器中看到以下内容:

If you’re not seeing this, you probably should try to resolve that before continuing. You’re welcome to use alternative database structures or data, but you’ll need to be careful with the rest of the examples.

如果您没有看到此消息,则可能应该尝试解决该问题,然后再继续。 欢迎您使用其他数据库结构或数据,但在其余示例中需要小心。

Now, let’s make a client for this data:

现在,让我们为这些数据创建一个客户端:

<!doctype html> <html lang="en"> <head> <title>Graterock</title> </head> <body> <ol class="cards"></ol> <script type="application/javascript"> fetch("http://localhost:8000/get") .then(function(response) { return response.json(); }) .then(function(json) { var cards = document.querySelector(".cards"); for (var i = 0; i < json.length; i++) { var card = document.createElement("li"); card.innerHTML = json[i].name; cards.appendChild(card); } }); </script> </body> </html>

We create an empty .cards list, which we’ll populate dynamically. For that, we use the HTML5 fetch function. It returns promises, which are a compositional pattern for callbacks.

我们创建一个空的.cards列表,并将其动态填充。 为此,我们使用HTML5 fetch功能。 它返回承诺,这是回调的一种组合模式。

This is just about the most technical part of all of this. I promise it’ll get easier, but if you want to learn more about fetch, check out https://developer.mozilla.org/en/docs/Web/API/Fetch_API. It works in recent versions of Chrome and Firefox, so use those to ensure these examples work!

这只是所有这些中最技术性的部分。 我保证它将变得更加简单,但是如果您想了解有关fetch更多信息,请查看https://developer.mozilla.org/en/docs/Web/API/Fetch_API 。 它可以在最新版本的Chrome和Firefox中运行,因此请使用这些版本确保这些示例正常工作!

We can work this client into our PHP server script:

我们可以将此客户端工作到我们PHP服务器脚本中:

$action = "/index"; $actions = ["/index", "/get"]; // ... if ($action == "/index") { header("Content-type: text/html"); print file_get_contents("index.html") and exit; }

Now we can load the client page, and get a list of cards, all from the same script. We don’t even need to change how we run the PHP development server – this should just work!

现在,我们可以加载客户端页面,并从同一脚本获取卡列表。 我们甚至不需要更改我们运行PHP开发服务器的方式-这应该可以正常工作!

Web插座盒 (A case for web sockets)

This is all pretty standard PHP/JavaScript stuff. Well, we are using a new JavaScript feature, but it’s pretty much the same as $.ajax.

这些都是非常标准PHP / JavaScript东西。 好吧,我们正在使用新JavaScript功能,但它与$.ajax几乎相同。

If we wanted to add realtime functionality to this, there are a few tricks we could try. Say we wanted to add realtime card removal; we could use Ajax requests to remove cards, and Ajax polling to refresh other clients automatically.

如果我们想为此添加实时功能,可以尝试一些技巧。 假设我们要添加实时卡移除功能; 我们可以使用Ajax请求删除卡,然后使用Ajax轮询自动刷新其他客户端。

This would work, but it would also add significant traffic to our HTTP server (even for duplicate responses).

这会起作用,但也会给我们的HTTP服务器增加大量流量(即使重复响应也是如此)。

Alternatively, we could open persistent connections between browsers and the server. We could then push new data from the server, and avoid polling continuously. These persistent connections can be web sockets, but there’s a problem…

或者,我们可以打开浏览器和服务器之间的持久连接。 然后,我们可以从服务器推送新数据,并避免连续轮询。 这些持久连接可以是Web套接字,但是有一个问题……

PHP was created, and is mostly used, to serve requests quickly. Most scripts and applications aren’t designed to be long-running processes. Because of this, conventional PHP scripts and applications are really inefficient at holding many connections open at once.

PHP已创建,并且主要用于快速处理请求。 大多数脚本和应用程序并非设计为长时间运行的进程。 因此,常规PHP脚本和应用程序实际上无法有效地一次打开许多连接。

This limitation often pushes developers towards other platforms (like NodeJS), or towards new architectures (like those provided by Icicle, ReactPHP, and AMPHP). This is one problem Socketize aims to solve.

这种限制通常将开发人员推向其他平台(例如NodeJS)或新架构(例如Icicle,ReactPHP和AMPHP提供的架构)。 这是Socketize旨在解决的问题。

套用救援! (Socketize to the rescue!)

For brevity, we’re going to use the un-authenticated version of Socketize. This means we’ll be able to read and write data without authenticating each user. Socketize supports authentication, and that’s the recommended (and even preferred) method.

为简便起见,我们将使用未经身份验证的Socketize版本。 这意味着我们将能够在不验证每个用户的情况下读取和写入数据。 Socketize支持身份验证,这是推荐(甚至首选)的方法。

Create a Socketize account, by going to https://socketize.com/register:

通过转到https://socketize.com/register创建一个Socketize帐户:

Then, go to https://socketize.com/dashboard/member/payload/generate, and generate a key for admin. Note this down. Go to https://socketize.com/dashboard/account/application for your account key (“Public Key”). Now we need to add some new JavaScript to our HTML page:

然后,转到https://socketize.com/dashboard/member/payload/generate ,并为admin生成一个密钥。 注意这一点。 转到https://socketize.com/dashboard/account/application作为您的帐户密钥(“公共密钥”)。 现在,我们需要在HTML页面中添加一些新JavaScript:

<script src="https://socketize.com/v1/socketize.min.js"></script> <script type="application/javascript"> var params = { "public_key": "[your_public_key]", "auth_params": { "username": "admin", "payload": "[your_admin_key]" } }; var socketize = new Socketize.client(params); socketize.on("login", function(user) { var user = socketize.getUser(); console.log(user); }); </script>

If you replace [your_public_key] and [your_admin_key] with the appropriate keys, and refresh the page, you should see a console message: “[Socketize] Connection established!”. You should also see an object describing the Socketize user account which is logged in. Note the user id.

如果用适当的密钥替换[your_public_key]和[your_admin_key] ,并刷新页面,则应该看到控制台消息:“ [Socketize] Connection建立!”。 您还应该看到一个对象,该对象描述了已登录的Socketize用户帐户。请注意用户id 。

What does this mean? Our HTML page is now connected to the Socketize database (for our account). We can read and write from named lists of messages. Let’s change our server script to write the initial list of cards to a named Socketize list. All Socketize API requests take the form of:

这是什么意思? 现在,我们HTML页面已连接到Socketize数据库(针对我们的帐户)。 我们可以从命名消息列表中进行读写。 让我们更改服务器脚本,将卡的初始列表写入命名的Socketize列表。 所有的Socketize API请求都采用以下形式:

curl "https://socketize.com/api/[method]?[parameters]" -u [your_public_key]:[your_private_key] -H "Accept: application/vnd.socketize.v1+json"

We can create a request function, to simplify this process:

我们可以创建一个request函数,以简化此过程:

function request($method, $endpoint, $parameters = "") { $public = "[your_public_key]"; $private = "[your_private_key]"; $auth = base64_encode("{$public}:{$private}"); $context = stream_context_create([ "http" => [ "method" => $method, "header" => [ "Authorization: Basic {$auth}", "Accept: application/vnd.socketize.v1+json", ] ] ]); $url = "https://socketize.com/api/{$endpoint}?{$parameters}"; $response = file_get_contents($url, false, $context); return json_decode($response, true); }

This method abstracts the common code for using the endpoints we’ve set up. We can see it in action, with the following requests:

此方法抽象出用于使用我们已设置的端点的通用代码。 我们可以看到它的实际应用,并具有以下要求:

$json = json_encode([ "id" => 1, "name" => "Mysterious Challenger", ]); request( "PUT", "push_on_list", "key=[your_user_id]:cards&value={$json}" ); request( "GET", "get_list_items", "key=[your_user_id]:cards" );

It’s probably best to use a robust HTTP request library, like GuzzlePHP, to make requests to third-party services. There’s also an official PHP SDK at: https://github.com/socketize/rest-php, but I prefer just to use these concise methods against the JSON API.

最好使用健壮的HTTP请求库(例如GuzzlePHP )向第三方服务发出请求。 在以下位置也有一个官方PHP SDK: https : //github.com/socketize/rest-php ,但是我更愿意仅对JSON API使用这些简洁的方法。

The request function takes an HTTP request method, Socketize API endpoint and query-string parameter. You’ll need to replace [your_public_key], [your_private_key], and [your_user_id] with the correct values. The two example calls should then work for you. cards is the name of the list to which we write this card object. You can think of Socketize like an HTTP version of an object store.

request函数采用HTTP请求方法,Socketize API端点和query-string参数。 您需要将[your_public_key] , [your_private_key]和[your_user_id]替换为正确的值。 然后,这两个示例调用将为您工作。 cards是我们向其写入此卡对象的列表的名称。 您可以将Socketize视为对象存储的HTTP版本。

We can adjust our HTML page to pull items from this list. While we’re at it, we can add a button to remove unwanted cards:

我们可以调整HTML页面以从该列表中提取项目。 进行操作时,我们可以添加一个按钮来删除不需要的卡片:

var socketize = new Socketize.client(params); var cards = document.querySelector(".cards"); cards.addEventListener("click", function(e){ if (e.target.matches(".card .remove")) { e.stopPropagation(); e.preventDefault(); socketize.publish("removed", e.target.dataset._id); socketize.updateListItem("cards", null, e.target.dataset._id); } }); socketize.subscribe("removed", function(_id) { var items = document.querySelectorAll(".card"); for (var i = 0; i < items.length; i++) { if (items[i].dataset._id == _id) { items[i].remove(); } } }); socketize.on("login", function(user) { var user = socketize.getUser(); socketize.getListItems("cards") .then(function(json) { for (var i = 0; i < json.length; i++) { var name = document.createElement("span"); name.className = "name"; name.innerHTML = json[i].name; var remove = document.createElement("a"); remove.dataset._id = json[i]._id; remove.innerHTML = "remove"; remove.className = "remove"; remove.href = "#"; var card = document.createElement("li"); card.dataset._id = json[i]._id; card.className = "card"; card.appendChild(name); card.appendChild(remove); cards.appendChild(card); } }); });

The match method works in recent versions of Chrome and Firefox. It’s not the only way to do it, but it is much cleaner than the alternative.

match方法适用于最新版本的Chrome和Firefox。 这不是唯一的方法,但是比其他方法干净得多。

In this example, we add a click event listener, intercepting clicks on .remove elements. This is called event delegation, and it’s super efficient! This way we don’t have to worry about removing event listeners on each .remove element.

在此示例中,我们添加了click事件侦听器,以拦截对.remove元素的单击。 这称为事件委托,它非常高效! 这样,我们不必担心在每个.remove元素上删除事件侦听器。

Each time a .remove element is clicked, we trigger an update to the Socketize list. At the same time, we publish a removal event. We also listen for removal events, so that we can update our list if other users remove cards.

每次单击.remove元素,我们都会触发Socketize列表的更新。 同时,我们发布了removal事件。 我们还会监听removal事件,以便在其他用户删除卡片时可以更新列表。

We can update the dataset properties of elements. These are the JavaScript representation of data-* HTML attributes. This way, we can store the Socketize _id of each card. Finally, we adjust the code to create new elements, and append the generated elements to the .cards element.

我们可以更新元素的dataset属性。 这些是data-* HTML属性JavaScript表示形式。 这样,我们可以存储每个卡的Socketize _id 。 最后,我们调整代码以创建新元素,并将生成的元素附加到.cards元素。

我们从这里去哪里? (Where do we go from here?)

This tiny example should illustrate how to begin doing useful stuff with Socketize. We added web socket communication (reading and writing to named lists, and publishing ad-hoc types of events). What’s awesome is that we did this without significantly altering our server-side code. We didn’t have to re-write for an event-loop. We just added a function or two, and we could push events to the browser.

这个小例子应该说明如何开始使用Socketize做有用的事情。 我们添加了Web套接字通信(读取和写入命名列表,以及发布临时类型的事件)。 令人敬畏的是,我们在没有明显改变服务器端代码的情况下做到了这一点。 我们不必为事件循环而重写。 我们只添加了一个或两个函数,就可以将事件推送到浏览器。

Think of the interesting things you could do with this. Every time you save a database record, you can push an event to the browser. You can connect gamers to each other. You can alert conference speakers when they are close to attendees with questions. All of this in a traditional PHP codebase.

想一想您可以做的有趣的事情。 每次保存数据库记录时,都可以将事件推送到浏览器。 您可以将游戏玩家彼此联系。 当有问题的与会者接近时,您可以提醒会议发言人。 所有这些都在传统PHP代码库中。

If you want to get better at this stuff, perhaps consider making an interface for adding and reordering cards. If you have questions, or can think of other interesting uses for this kind of persistent communication; leave a comment below.

如果您想在这些方面变得更好,则可以考虑创建一个用于添加和重新排序卡的界面。 如果您有疑问,或者可以想到这种持续性交流的其他有趣用途; 在下面发表评论。

翻译自: https://www.sitepoint.com/websockets-in-your-synchronous-site/

websocket同步

最新回复(0)